Tempo di esecuzione
Panoramica dell’architettura
Il runtime Goa-AI orchestra il ciclo plan/execute/resume, applica le policy, gestisce lo stato e coordina engine, planner, strumenti, memoria, hook e moduli funzionali.
| Strato | Responsabilità |
|---|---|
| DSL + Codegen | Produrre i registri degli agenti, le specifiche/codici degli strumenti, i flussi di lavoro, gli adattatori MCP |
| Runtime Core | Orchestrano il ciclo di pianificazione/avvio/ripresa, l’applicazione delle politiche, gli hook, la memoria, lo streaming |
| Workflow Engine Adapter | L’adattatore temporale implementa engine.Engine; altri motori possono essere collegati |
| Archivio del runtime host | Salva insieme ambito della sessione, stato dell’esecuzione, checkpoint e record immutabili |
| Moduli di funzionalità | Integrazioni opzionali (MCP, Pulse, negozi Mongo, fornitori di modelli) |
Architettura agenziale di alto livello
In fase di esecuzione, Goa-AI organizza il sistema attorno a un piccolo insieme di costrutti componibili:
Agenti: Orchestratori di lunga durata identificati da
agent.Ident(ad esempio,service.chat). Ogni agente possiede un pianificatore, una politica di esecuzione, flussi di lavoro generati e registrazioni di strumenti.Esecuzioni: Una singola esecuzione di un agente. Le esecuzioni sono identificate da un
RunIDe tracciate tramiterun.Contexterun.Handle. Le esecuzioni con sessione sono raggruppate daSessionIDeTurnIDper formare conversazioni; le esecuzioni one-shot sono esplicitamente senza sessione.Toolsets e strumenti: Raccolte nominate di funzionalità, identificate da
tools.Ident(service.toolset.tool). Gli insiemi di strumenti supportati da servizi chiamano le API; gli insiemi di strumenti supportati da agenti eseguono altri agenti come strumenti.Completion: contratti tipizzati di proprietà del servizio per l’output finale diretto dell’assistente, generati in
gen/<service>/completions. Gli helper collegano lo structured output imposto dal provider alle richieste unary e di streaming diretto, quindi decodificano il payload canonico con i codec generati.Planner: il livello strategico guidato dall’LLM che implementa
PlanStart/PlanResume. I planner decidono quando chiamare strumenti o rispondere direttamente; il runtime applica limiti e budget temporali.Albero di esecuzione e agente come strumento: Quando un agente chiama un altro agente come strumento, il runtime avvia una vera e propria esecuzione figlia con il proprio
RunID. Il genitoreToolResultporta unRunLink(*run.Handle) che punta al figlio e viene emesso un evento di streamingchild_run_linkedper correlare la chiamata dello strumento genitore con ilRunIDfiglio.Flussi e profili: Goa-AI pubblica valori
stream.Eventtipizzati in uno stream di proprietà della sessione (session/<session_id>). Gli eventi includonoRunIDeSessionIDe il runtime emetterun_stream_endcome marcatore esplicito per chiudere SSE/WebSocket senza timer.stream.StreamProfileseleziona quali tipi di eventi sono visibili per un determinato pubblico (chat UI, debug, metriche).
Avvio rapido
package main
import (
"context"
"time"
chat "example.com/assistant/gen/orchestrator/agents/chat"
"goa.design/goa-ai/runtime/agent/model"
"goa.design/goa-ai/runtime/agent/runtime"
storageinmem "goa.design/goa-ai/runtime/agent/storage/inmem"
)
func main() {
// In-memory engine is the default; pass WithEngine for Temporal or custom engines.
store := storageinmem.New()
rt := runtime.New(store)
ctx := context.Background()
err := chat.RegisterChatAgent(ctx, rt, chat.ChatAgentConfig{Planner: newChatPlanner()})
if err != nil {
panic(err)
}
// Sessions are first-class: create a session before starting runs under it.
if _, err := store.CreateSession(ctx, "session-1", time.Now().UTC()); err != nil {
panic(err)
}
client := chat.NewClient(rt)
out, err := client.Run(ctx, "session-1", []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Summarize the latest status."}},
}})
if err != nil {
panic(err)
}
// Use out.RunID, out.Final (the assistant message), etc.
}
Completion dirette tipizzate
Non tutte le interazioni strutturate devono essere modellate come chiamate a
strumenti. Quando un servizio richiede una risposta finale tipizzata
dell’assistente, dichiarare Completion(...) nel DSL e rigenerare.
goa gen emette gen/<service>/completions con tipi di risultato e unioni,
schemi e codec privati, helper Complete<Name>(ctx, client, req), helper
StreamComplete<Name>(ctx, client, req) e <Name>Example() quando il
risultato radice ha un Example(...) dichiarato. Un servizio può dichiarare
completion senza dichiarare alcun Agent(...).
Gli helper clonano la richiesta, allegano metadati neutrali rispetto al
provider, chiamano il model.Client e decodificano il payload tipizzato:
resp, err := taskcompletion.CompleteDraftFromTranscript(ctx, modelClient, &model.Request{
Messages: []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Create a startup investigation task."}},
}},
})
if err != nil {
panic(err)
}
fmt.Println(resp.Value.Name)
Ogni model.StructuredOutput di basso livello richiede un nome non vuoto. Gli
helper generati lo derivano dal DSL validato. Una completion unary effettua
esattamente una chiamata al modello. Un JSON non valido restituisce
planner.OutputContractError, non riprovabile, e una risposta nil; non avvia
mai una richiesta di correzione. In caso di successo,
resp.ModelResponse contiene la risposta esatta del provider e l’uso dei
token.
Le completion in streaming restituiscono completion.Streamer[T]. Recv
espone frammenti di anteprima; Value() resta indisponibile finché lo stream
non termina e la risposta terminale non concorda con la completion finale:
stream, err := taskcompletion.StreamCompleteDraftFromTranscript(ctx, modelClient, &model.Request{
Messages: []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Create a startup investigation task."}},
}},
})
if err != nil {
panic(err)
}
defer stream.Close()
for {
chunk, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
panic(err)
}
// Render preview completion_delta chunks here when useful.
_ = chunk
}
value, ok := stream.Value()
if !ok {
panic("completion stream ended without a typed value")
}
fmt.Println(value.Name)
I nomi delle completion sono validati al confine DSL: 1-64 caratteri ASCII,
solo lettere, cifre, _ e -, con inizio alfanumerico. Gli helper rifiutano
richieste con strumenti o StructuredOutput fornito dal chiamante. Il wrapper
espone Value() solo dopo fine stream pulita e validazione completa. I
provider senza structured output restituiscono
model.ErrStructuredOutputUnsupported.
Solo client vs. Lavoratore
Due ruoli utilizzano il runtime:
- Solo cliente (invia le esecuzioni): Costruisce un runtime con un motore compatibile con i client e non registra agenti. Usa il
<agent>.NewClient(rt)generato, che porta con sé laAgentDefinitiongenerata e condivisa con i worker remoti. - Worker (esegue esecuzioni): Costruisce un runtime con un motore capace di lavorare, registra toolset e agenti, quindi chiude la registrazione affinché il polling inizi solo quando il registro locale del runtime è completo.
Ogni AgentDefinition generata è il contratto completo e immutabile di un
agente. Contiene il nome del workflow, la coda di attività predefinita, i
contratti degli strumenti generati, le etichette obbligatorie, la policy di
completion e le definizioni di tutti gli agenti figli raggiungibili. I chiamanti
la usano per convalidare e indirizzare il lavoro prima che il motore accetti il
workflow; i worker usano lo stesso valore per registrarlo. Una singola
esecuzione può scegliere un’altra coda con WithTaskQueue, ma le registrazioni
scritte a mano non devono definire una seconda route o un secondo grafo di
agenti figli.
Esempio solo client
rt := runtime.New(runtimeStore, runtime.WithEngine(temporalClient)) // engine client
// The host session service has already created "s1".
// No agent registration is needed in a caller-only process.
client := chat.NewClient(rt)
out, err := client.Run(ctx, "s1", msgs)
Run one-shot senza sessione
Usa StartOneShot e OneShotRun quando vuoi lavoro durevole che non sia associato a una sessione esistente.
Start/Runsono con sessione: richiedono unSessionIDconcreto, partecipano al lifecycle della sessione ed emettono eventi di stream con scope di sessione.StartOneShot/OneShotRunsono senza sessione: non accettanoSessionIDe non ne creano uno. Prima di eseguire il lavoro, lo storage integrato salva i metadati completi senza sessione e il recordRunStarted, così l’esecuzione può essere consultata tramiteRunID.- L’applicazione host crea le sessioni prima del lavoro; i runtime degli agenti non creano, terminano o eliminano sessioni.
- Il motore accetta un workflow radice prima che la prima activity registri l’esecuzione. Nessuno stato
pendingviene creato prima dell’ammissione. - Gli avvii radice, figlio e one-shot sono operazioni distinte. L’avvio figlio salva il collegamento al padre; one-shot salva metadati completi senza sessione.
- Il motivo di annullamento è write-once. Una ripetizione identica riesce; un motivo diverso produce un conflitto.
- Sospensione e termine salvano il nuovo stato insieme al record immutabile corrispondente.
StartOneShotrestituisce subito unengine.WorkflowHandle.OneShotRunè il wrapper bloccante che chiamahandle.Wait(ctx)per te.
client := chat.NewClient(rt)
handle, err := client.StartOneShot(ctx, msgs,
runtime.WithRunID("run-123"),
runtime.WithLabels(map[string]string{"tenant": "acme"}),
)
if err != nil {
panic(err)
}
out, err := handle.Wait(ctx)
if err != nil {
panic(err)
}
fmt.Println(out.RunID)
Esempio di worker
eng, err := temporal.NewWorker(temporal.Options{
ClientOptions: &client.Options{HostPort: "temporal:7233", Namespace: "default"},
WorkerOptions: temporal.WorkerOptions{TaskQueue: "orchestrator.chat"},
})
if err != nil {
panic(err)
}
defer eng.Close()
rt := runtime.New(runtimeStore, runtime.WithEngine(eng))
if err := chat.RegisterUsedToolsets(ctx, rt /* executors... */); err != nil {
panic(err)
}
if err := chat.RegisterChatAgent(ctx, rt, chat.ChatAgentConfig{Planner: myPlanner}); err != nil {
panic(err)
}
if err := rt.Seal(ctx); err != nil {
panic(err)
}
Ciclo Pianifica → Esegui → Riprendi
- Il motore accetta un workflow per l’agente, in memoria o in Temporal.
- La prima activity salva l’identità e il primo record permanente tramite
StartRootRun,StartChildRun,StartOneShotRunoStartOneShotChildRun. Ogni workflow accettato salvaRunStarted. Memoria e sessioni descrive i tre modi validi di salvare i motivi e i record di richiesta di annullamento. - Il runtime chiama
PlanStartconPrepareMessagese unrun.ContextcontenenteRunID,SessionID,TurnID, etichette e limiti di policy. Il pianificatore chiama la funzione se gli serve la cronologia. - Pianifica le chiamate agli strumenti usando i codec generati.
- Chiama
PlanResumecon gli output che restano visibili al planner. Gli strumenti con budget sono visibili per impostazione predefinita. Un errore di uno strumento di bookkeeping pianifica un altro turno secondoToolFailure.Recovery.Action: correzione, nuova pianificazione senza quello strumento oppure finalizzazione. Il ciclo continua finché il planner restituisce una risposta finale, un risultato finale oppure uno strumentoTerminalRunriesce. Se limiti o deadline impongono la finalizzazione, il planner può chiudere tramite strumenti terminali di bookkeeping. - I ganci e i sottoscrittori del flusso emettono eventi (pensieri del pianificatore, avvio/aggiornamento/fine dello strumento, attese, utilizzo, flusso di lavoro, collegamenti tra agenti e corse) e, se configurati, persistono le voci di trascrizione e i metadati della corsa.
Fasi dell’esecuzione
Quando un’esecuzione avanza nel ciclo di pianificazione/esecuzione/ripresa, passa attraverso una serie di fasi del ciclo di vita. Queste fasi forniscono una visibilità a grana fine del punto in cui si trova un’esecuzione, consentendo alle interfacce utente di mostrare indicatori di avanzamento di alto livello.
Valori delle fasi
| Fase | Descrizione |
|---|---|
prompted | L’input è stato ricevuto e l’esecuzione sta per iniziare la pianificazione |
planning | Il pianificatore sta decidendo se e come chiamare gli strumenti o rispondere direttamente |
executing_tools | Gli strumenti (compresi gli agenti nidificati) sono attualmente in esecuzione |
synthesizing | Il pianificatore sta sintetizzando una risposta finale senza programmare strumenti aggiuntivi |
completed | L’esecuzione è stata completata con successo |
failed | L’esecuzione è fallita |
canceled | L’esecuzione è stata annullata |
Transizioni di fase
Una tipica esecuzione di successo segue questa progressione:
prompted → planning → executing_tools → planning → synthesizing → completed
↑__________________|
(loop while tools needed)
Il runtime emette eventi RunPhaseChanged per le fasi non terminali (ad esempio planning, executing_tools, synthesizing) così che gli abbonati allo stream possano seguire i progressi in tempo reale.
Fase vs Stato
Le fasi sono distinte da run.Status:
- Status (
running,suspended,completed,failed,canceled) è lo stato del ciclo di vita a grana grossa memorizzato nei metadati durevoli dell’esecuzione. Non esiste uno statopendingprima dell’ammissione. - Phase fornisce una visibilità a grana più fine del ciclo di esecuzione, destinata alle superfici di streaming/UX
Eventi di ciclo di vita: cambi di fase vs completamento
Il runtime emette:
RunPhaseChangedper transizioni di fase non terminali.RunCompleteduna sola volta per run per lo stato terminale (success / failed / canceled).
I subscriber di stream traducono entrambi in eventi workflow (stream.WorkflowPayload):
- Aggiornamenti non terminali (da
RunPhaseChanged): solophase. - Aggiornamento terminale (da
RunCompleted):status+phaseterminale, con campi d’errore strutturati in caso di failure.
Mapping dello status terminale
status="success"→phase="completed"status="failed"→phase="failed"status="canceled"→phase="canceled"
La cancellazione non è un errore
Per status="canceled", il payload stream non deve includere un error user-facing. I consumer devono trattare la cancellazione come uno stato terminale non errore.
Le failure sono strutturate
Per status="failed", il payload stream include:
error_kind: classificatore stabile per UX/decisioni (kinds provider comerate_limited,unavailable, o kinds runtime cometimeout/internal)retryable: se un retry può riuscire senza cambiare inputerror: messaggio user-safe (render diretto)debug_error: testo diagnostico dell’errore; l’applicazione decide chi può vederlo
Identità terminale
RunCompleted include Labels: le etichette con ambito di run fornite
all’avvio della run (RunInput.Labels, impostate con
runtime.WithLabels(...)), nil quando la run non ne aveva. I sottoscrittori di
completamento possono attribuire l’esito terminale — success, failed o
canceled — senza mantenere una propria mappa da run-ID a identità. Le stesse
etichette sono esposte su run.Snapshot.Labels per i lettori in polling,
ricostruite dal record durevole RunStarted, così l’identità della run
sopravvive ai riavvii del processo su entrambi gli engine. Le etichette unite
dalle decisioni di policy a metà run non sono incluse; restano osservabili
tramite gli eventi PolicyDecision.
Diagnostica degli errori
Goa-AI conserva integralmente i messaggi diagnostici e il testo degli errori tipizzati del provider in UTF-8 valido, senza limiti di lunghezza per campo. L’applicazione decide cosa registra la propria strumentazione e chi può leggerlo o visualizzarlo. Gli span del planner e delle attività Temporal ricevono l’errore originale prima del trasporto del workflow o della conversione dell’errore. Il replay del workflow non emette nuovamente tali diagnostiche. I riepiloghi da visualizzare, la classificazione, la possibilità di riprovare e il recupero del modello restano invariati; il testo diagnostico non è un’istruzione di correzione per il modello.
Rifiuti locali delle richieste al modello
Usare model.NewRequestValidationError(cause) solo quando una convalida
dell’applicazione rifiuta una richiesta al modello prima che un provider la
accetti. Un adattatore di modello remoto può ricostruire questo tipo dall’errore
esplicito di convalida delle richieste del proprio servizio. La causa è
obbligatoria: Unwrap() espone l’errore originale ed Error() restituisce la
diagnostica completa. Questo tipo non contiene nome del provider, stato HTTP,
impostazioni di retry o istruzioni di recupero.
Non usarlo per errori di rete, observer, cancellazione o provider. I validatori e
gli adattatori esistenti mantengono il proprio comportamento, a meno che il
responsabile non identifichi esplicitamente un rifiuto locale. I rifiuti effettivi
del provider usano ancora model.ProviderError; l’output non valido del modello
o del planner mantiene il proprio contratto distinto di convalida dell’output.
La run termina con il tipo model_request, Retryable: false e senza provider,
operazione, codice del provider o stato HTTP. Il riepilogo predefinito è
“The AI request could not be prepared.” L’applicazione può modificare
hooks.PublicErrorModelRequest all’avvio del processo; DebugMessage conserva
la diagnostica completa. Né l’esecuzione dei tool né gli agenti annidati
trasformano questo errore in un retry o in una chiamata aggiuntiva al modello
per correggerlo. Il testo già pubblicato da una chiamata precedente al modello
nella stessa attività del planner viene conservato prima che la run termini.
Un ApplicationError Temporal personalizzato restituito direttamente mantiene
la classificazione e la policy di retry esistenti dell’applicazione; questo
tipo non sostituisce tale errore esterno esplicito.
Temporal salva l’errore come goa_ai.request_validation_error, con
NonRetryable: true e la diagnostica valida completa nel messaggio, senza
dettagli o un oggetto causa. I lettori rifiutano un valore salvato che consente
retry, contiene dettagli o una causa, oppure presenta UTF-8 non valido. La
codifica delle diagnostiche e i limiti esterni di dimensione degli errori
restano applicabili; non viene introdotto un nuovo limite di testo. Gli errori
di provider, output, generici e di cancellazione già salvati non vengono
reinterpretati.
Aggiornare i worker prima che gli adattatori inizino a produrre questo tipo. I worker precedenti non possono leggere la classificazione salvata e non devono elaborare cronologie che la contengono, nemmeno tornando a una versione precedente. Assegnare tali cronologie solo ai worker aggiornati. Non sono previsti cambiamenti dell’API generata, migrazioni del database o modalità di compatibilità, e l’aggiornamento non riclassifica gli errori salvati in precedenza.
Formati degli errori salvati
I nuovi record OutputContractFailure, ModelOutputRejected e
PlannerOutputRejected usano ReasonVersion="goa_ai.rejection_reason.v2".
Reason conserva il testo esatto della causa selezionata, identificato da
ReasonSHA256 e ReasonSize. Il testo valido lascia ReasonOmitted vuoto;
il testo UTF-8 non valido produce un Reason vuoto e
ReasonOmitted="invalid_utf8". I nuovi record non usano size_limit per omettere
una causa lunga.
I nuovi errori Temporal di provider, generici, di contratto dell’output e di tipo riservato non valido usano questi quattro tipi privati di errore applicativo:
goa_ai.provider_error.v3goa_ai.generic_error.v3goa_ai.output_contract_error.v3goa_ai.invalid_reserved_error.v3
Il tipo seleziona il formato dei dettagli salvati. I dettagli generici e del provider conservano il proprio testo come stringhe semplici, separatamente dal messaggio diagnostico esterno. Il testo UTF-8 non valido diventa un avviso esplicito di testo non disponibile con hash e numero di byte originali, senza caratteri di sostituzione silenziosi. Questi formati non serializzano cause Go arbitrarie, oggetti di errore dell’SDK o dettagli personalizzati dell’applicazione. Conservare testo valido esatto non significa memorizzare byte arbitrari.
Limiti del trasporto e dell’applicazione
Il limite esistente per argomenti e risultati completi del workflow continua ad applicarsi all’intero valore codificato, compresi i campi associati. Un risultato del planner troppo grande restituisce un errore esplicito di budget di trasporto; il testo diagnostico non viene salvato accorciandolo silenziosamente.
Gli oggetti di errore nativi di Temporal usano un convertitore di errori
dell’SDK separato, non il controllo delle dimensioni degli argomenti e risultati
del workflow. Goa-AI non aggiunge limiti di dimensione o controlli preliminari
per questi errori nativi. Il FailureConverter configurato dall’applicazione,
incluso il comportamento di rifiuto, resta sotto il controllo dell’applicazione.
I limiti di richieste e cronologia di Temporal possono rifiutare errori grandi; lo stato di retry di un’attività in attesa può conservare un errore accorciato dal server. Anche la strumentazione ha limiti di campionamento, esportazione e backend. Conservare il testo nel framework non garantisce archiviazione o consegna illimitate, né recupera il testo omesso in precedenza.
Aggiornamento dei worker e cronologie salvate
I nuovi lettori mantengono il comportamento dei record di rifiuto senza versione e v1, e dei tipi Temporal storici con dettagli v1/v2. La decodifica e il replay non riscrivono tali record, non cambiano i byte pubblicati e non ripristinano il testo mancante. Le vecchie regole di omissione e convalida continuano ad applicarsi ai vecchi formati.
Gli errori terminali già salvati conservano i byte originali. Un workflow che legge vecchi metadati di rifiuto ma termina per la prima volta dopo l’aggiornamento scrive il tipo corrente di errore terminale. Un replay riuscito non dimostra che i vecchi e i nuovi comandi di errore terminale abbiano dettagli codificati identici.
Aggiorna i consumer degli hook che convalidano i record e i worker di workflow e attività prima che ricevano i nuovi formati. Non usare nuovi writer insieme a vecchi lettori incompatibili sulle stesse code di attività; usa il routing per versione dei worker o la procedura verificata dell’applicazione per svuotare le code e sostituire i worker. Un rollback deve mantenere lettori capaci di interpretare tutti i formati già scritti. Conserva i decoder storici finché i record delle esecuzioni o le cronologie dei workflow supportati ne hanno bisogno; la sola sostituzione dei worker non elimina questo requisito.
Politiche, cappucci ed etichette
Politica di esecuzione in tempo di progettazione
In fase di progettazione, si configurano le politiche per agente con RunPolicy:
Agent("chat", "Conversational runner", func() {
RunPolicy(func() {
DefaultCaps(
MaxToolCalls(8),
MaxRecoveryTurns(3),
)
TimeBudget("2m")
InterruptsAllowed(true)
})
})
Questo diventa un runtime.RunPolicy allegato alla registrazione dell’agente:
Limiti:
MaxToolCallslimita il numero totale di chiamate agli strumenti con budget per ogni esecuzione.MaxRecoveryTurnslimita le nuove chiamate al pianificatore dopo il rifiuto del risultato di uno strumento o di una risposta del modello. Il lavoro riuscito con budget ripristina questa disponibilità salvo quando resta attivo un errorefinish; le pagine ottenute non ripristinano mai la disponibilità di quell’errore. Gli strumentiBookkeeping()non consumano nessuno dei due budget.Bilancio di tempo:
TimeBudget- budget di tempo per la corsa.FinalizerGrace(solo per la corsa) - finestra riservata opzionale per la finalizzazione.Interruzioni:
InterruptsAllowed- opt-in per pausa/ripresa.Completamento terminale del run: gli strumenti dichiarati
TerminalRun()diventano automaticamente bookkeeping e chiudono il run dopo una chiamata riuscita, senza un turnoPlanResumesuccessivo. Un commit terminale può quindi essere ammesso senza budget di retrieval residuo. Durante la finalizzazione dovuta a scadenze o limiti, il runtime ammette solo chiamate terminali di bookkeeping, le esegue nella finestra restante dell’hard deadline e chiude il run solo se ogni effetto terminale riesce. Prima dell’esecuzione, il runtime scrive l’esattoplanner.TerminationReasoninruntime.FinalizationReasonLabel(goa-ai.finalization_reason). Etichette del run o della policy e output del planner o del modello non possono scegliere né sostituire questo valore; le chiamate ordinarie non lo ricevono.I consumer di chiamate terminali dovute a limiti fissi o scelte dal planner, incluso
tool_failure, usanoruntime.FinalizationReasonLabel. Distribuire insieme consumer e worker quando cambia questo contratto di esecuzione.Comportamento dei campi mancanti:
OnMissingFields- regola cosa succede quando la validazione indica campi mancanti.
Sovrascritture dei criteri di runtime
In alcuni ambienti si può desiderare di rendere più rigide o meno rigide le politiche senza modificare il progetto. L’API rt.OverridePolicy consente di modificare i criteri a livello locale:
err := rt.OverridePolicy(chat.AgentID, runtime.RunPolicy{
MaxToolCalls: 3,
MaxRecoveryTurns: 1,
InterruptsAllowed: true,
})
Ambito di applicazione: Le sovrascritte sono locali all’istanza del runtime corrente e hanno effetto solo sulle esecuzioni successive. Non persistono nei riavvii del processo e non si propagano ad altri worker.
Campi sovrascrivibili:
| Campo | Descrizione |
|---|---|
MaxToolCalls | Chiamate massime agli strumenti con budget per esecuzione (gli strumenti Bookkeeping() sono esenti) |
MaxRecoveryTurns | Nuove chiamate al pianificatore dopo un output rifiutato |
TimeBudget | Budget del wall-clock per la corsa |
FinalizerGrace | Finestra riservata per la finalizzazione |
InterruptsAllowed | Abilita la funzionalità di pausa/ripresa |
Vengono applicati solo i campi non nulli (e InterruptsAllowed quando true). Ciò consente di sovrascrivere selettivamente i campi senza influire sulle altre impostazioni del criterio.
Casi d’uso:
- Arretramenti temporanei durante il throttling del provider
- Test A/B di diverse configurazioni di criteri
- Sviluppo/debug con vincoli rilassati
- Personalizzazione dei criteri per inquilino in fase di runtime
Etichette e motori di policy
Goa-AI si integra con motori di policy collegabili tramite policy.Engine. Le
policy ricevono i metadati degli strumenti (ID, tag), il contesto di esecuzione
(SessionID, TurnID, etichette) e il ToolFailure strutturato dopo
un’esecuzione fallita.
Le etichette confluiscono in:
run.Context.Labels- disponibili per i pianificatori durante una sessione- input dell’attività di tool (
api.ToolInput.Labels) – clonato nelle esecuzioni inviate; le chiamate di finalizzazione ricevono anche il motivo di proprietà del runtime inruntime.FinalizationReasonLabel - L’archivio del runtime (
storage.Store) aggiunge record immutabili perRunID. I metodi del ciclo di vita salvano stato, checkpoint o annullamento insieme al record corrispondente. - completamento terminale e snapshot - le etichette iniziali riemergono alla fine della run su
hooks.RunCompletedEvent.Labelserun.Snapshot.Labels, così gli hook di completamento e i lettori diGetRunSnapshotrecuperano l’identità della run senza tracciamento fuori banda
Usa WithRestrictToTool quando un flusso di correzione deve esporre esattamente
uno strumento:
out, err := client.Run(ctx, "session-1", messages,
runtime.WithRestrictToTool(searchspecs.Search),
)
Questa policy dell’applicazione vale per l’intera esecuzione. Gli errori degli
strumenti usano un contratto distinto: ToolFailure.Recovery.Action sceglie la
correzione, una nuova pianificazione o la conclusione, e il runtime impone il
catalogo risultante nel turno successivo.
Esecuzione dello strumento
- Set di strumenti nativi: L’utente scrive le implementazioni; il runtime gestisce la decodifica degli argomenti digitati utilizzando i codec generati
- Agent-as-tool: Gli strumenti agent-tool generati eseguono gli agenti provider come esecuzioni figlio (in linea dal punto di vista del pianificatore) e adattano il loro
RunOutputin unplanner.ToolResultcon un handleRunLinkall’esecuzione figlio - Mcp toolsets: Il runtime inoltra il JSON canonico ai chiamanti generati; i chiamanti gestiscono il trasporto
Tool payload defaults
Tool payload decoding follows Goa’s decode-body → transform pattern and applies Goa-style defaults deterministically for tool payloads.
See Tool Payload Defaults for the contract and codegen invariants.
Risultati degli strumenti delimitati
Gli strumenti paginati dichiarano BoundedResult(...). Il provider imposta il
proprio cursor privato in Bounds.NextCursor; il runtime espone al modello un
riferimento breve legato a run, sessione e strumento. La chiamata successiva
contiene solo quel riferimento nel campo cursor. Il runtime ne verifica validità
e ambito, ripristina gli argomenti originali e inserisce il cursor privato prima
dell’esecuzione. Il riferimento non può essere riutilizzato né spostato su
un’altra sessione o un altro strumento.
Contratti runtime dei prompt
La gestione dei prompt e nativa del runtime e versionata:
runtime.PromptRegistryconserva registrazioni immutabili delle prompt spec baseline (prompt.PromptSpec).runtime.WithPromptStore(prompt.Store)abilita la risoluzione degli override con scope (session->facility->org-> global).- I planner chiamano
PlannerContext.RenderPrompt(ctx, id, data)per risolvere e rendere il contenuto. - Il contenuto renderizzato include metadati
prompt.PromptRefper provenance; i planner possono allegarli amodel.Request.PromptRefs.
messages, err := input.PrepareMessages()
if err != nil {
return nil, err
}
content, err := input.Agent.RenderPrompt(ctx, "assistant.system", map[string]any{
"AssistantName": "Ops Assistant",
})
if err != nil {
return nil, err
}
resp, err := modelClient.Complete(ctx, &model.Request{
RunID: input.RunContext.RunID,
Messages: messages,
PromptRefs: []prompt.PromptRef{content.Ref},
})
PromptRefs indica quali versioni renderizzate dei prompt hanno influenzato una richiesta; non fa parte del payload del provider. Il runtime lo deriva dai record prompt_rendered e dai collegamenti padre-figlio, senza mantenere una seconda lista che possa divergere.
Il rendering non scrive mai nello storage del runtime. Tutti i percorsi usano
prompt.RenderRecorder per creare lo stesso prompt.RenderEvent con ID,
versione e ambito del prompt risolto:
- il codice dell’applicazione che renderizza i messaggi iniziali passa
recorder.Events()tramiteruntime.WithRenderedPromptsinsieme a quei messaggi; - le activity del pianificatore restituiscono i propri eventi con il risultato;
- la preparazione del prompt di un agente figlio avviene in una activity e restituisce il testo renderizzato e i suoi eventi nell’input del figlio;
RunOneShotregistra i rendering eseguiti dal proprio callback.
Il workflow salva ogni evento accettato come lo stesso record
PromptRendered. Il percorso iniziale non applica una regola di rendering
diversa: consegna soltanto un evento creato prima dell’avvio del workflow. La
preparazione del figlio avviene in una activity così il replay di Temporal
riutilizza testo ed eventi già presenti nella cronologia, senza leggere una
versione più recente del prompt.
RenderRecorder.Events restituisce i rendering completati in un ordine stabile
per ID del prompt, versione, sessione e ambito. L’ordine di completamento di
rendering concorrenti non può quindi cambiare la richiesta esatta di avvio del
workflow.
Memoria, flusso, telemetria
Hook bus pubblica eventi strutturati per l’intero ciclo di vita: avvio e completamento del run, cambi di fase,
prompt_rendered, pianificazione/risultati/aggiornamenti degli strumenti, note e thinking del planner, attese, direttive di recuperoToolFailuree collegamenti agent-as-tool.i Memory Store (
memory.Store) sottoscrivono e aggiungono eventi di memoria durevoli (messaggi di utenti/assistenti, chiamate agli strumenti, risultati degli strumenti, note del pianificatore, riflessioni) per(agentID, RunID).Lo storage del runtime (
storage.Store) è unico e appartiene all’applicazione host. Per ogniRunIDaggiunge record che non possono cambiare dopo l’inserimento, destinati alle UI di audit e debug e alla consultazione delle esecuzioni. I suoi metodi del ciclo di vita salvano stato, checkpoint o modifica dell’annullamento insieme al record immutabile corrispondente in una sola operazione.gli Stream sinks (
stream.Sink, ad esempio Pulse o SSE/WebSocket personalizzati) ricevono i valoristream.Eventtipizzati prodotti dallostream.Subscriber. UnStreamProfilecontrolla quali tipi di eventi vengono emessi.La trascrizione durevole conserva esattamente ogni risposta selezionata del provider. Quando un messaggio dell’assistente contiene una chiamata a uno strumento, il testo resta nella trascrizione per essere riprodotto al provider, ma non viene emesso come risposta visibile all’utente. Gli eventi dello strumento e di attesa presentano quel passaggio non terminale. Solo i messaggi dell’assistente senza chiamate a strumenti producono eventi di testo dell’assistente confermati.
Telemetria: La registrazione, le metriche e la tracciabilità dei flussi di lavoro e delle attività da un capo all’altro di OTEL.
Suggerimenti per le chiamate ai tool (DisplayHint)
Le chiamate ai tool possono includere un DisplayHint rivolto all’utente (ad esempio per UI).
Contratto:
- I costruttori di hook non renderizzano suggerimenti. Gli eventi di pianificazione dei tool hanno
DisplayHint==""per impostazione predefinita. - Il runtime arricchisce e persiste un suggerimento di chiamata duraturo al momento della pubblicazione a partire dal template tipizzato quando la decodifica del payload riesce.
- La registrazione dei tool richiede un titolo di metadati non vuoto. Se la decodifica tipizzata fallisce o non è registrato alcun template, il runtime usa quel titolo come display hint. I payload malformati continuano a fallire al confine del tool; il titolo di metadati serve solo a mantenere renderizzabile il lavoro tentato. I suggerimenti non vengono mai renderizzati a partire da JSON grezzo.
- Se un producer imposta esplicitamente
DisplayHint(non vuoto) prima di pubblicare l’evento hook, il runtime lo considera autorevole e non lo sovrascrive. - Per variazioni per-consumer (ad esempio testo UI), configurare
runtime.WithHintOverridessul runtime. Gli override hanno la precedenza sui template DSL per gli eventitool_startstreammati.
Consumare lo stream di sessione (Pulse)
In produzione, il pattern tipico è:
- consumare lo stream di sessione (
session/<session_id>) da un bus condiviso (Pulse / Redis Streams) - filtrare per
run_idper costruire lane/card per esecuzione - chiudere SSE/WebSocket quando si osserva
run_stream_endper ilrun_idattivo
import "goa.design/goa-ai/runtime/agent/stream"
events, errs, cancel, err := sub.Subscribe(ctx, "session/session-123")
if err != nil {
panic(err)
}
defer cancel()
activeRunID := "run-123"
for {
select {
case evt, ok := <-events:
if !ok {
return
}
if evt.Type() == stream.EventRunStreamEnd && evt.RunID() == activeRunID {
return
}
case err := <-errs:
panic(err)
}
}
Astrazione del motore
- In-memory: Ciclo di sviluppo veloce, nessun supporto esterno
- Temporal: esecuzione durevole, replay, retry delle activity, segnali e worker; gli adattatori collegano le activity e propagano il contesto
I workflow degli agenti Goa-AI hanno un solo tentativo. Il runtime ritenta le singole activity quando il loro contratto lo permette, ma non riavvia mai un intero workflow dell’agente dopo un errore. Un riavvio completo potrebbe ripetere gli effetti dei tool o entrare in conflitto con il record finale già salvato dal primo tentativo.
Temporizzazione semantica vs liveness di Temporal
Goa-AI mantiene il contratto pubblico del runtime indipendente dal motore:
RunPolicy.Timing.PlaneRunPolicy.Timing.Toolssono budget semantici per tentativoruntime.WithTiming(...)sostituisce tali budget semantici per una run- I client generati usano la coda predefinita dell’agente. Passa
runtime.WithTaskQueue("orchestrator.chat")a una chiamataStartoRunquando una singola esecuzione deve usare un’altra coda
Se si usa l’adattatore Temporal e occorre regolare l’attesa in coda o la liveness, queste impostazioni vanno configurate direttamente sul motore Temporal:
eng, err := temporal.NewWorker(temporal.Options{
ClientOptions: &client.Options{
HostPort: "temporal:7233",
Namespace: "default",
},
WorkerOptions: temporal.WorkerOptions{
TaskQueue: "orchestrator.chat",
},
ActivityDefaults: temporal.ActivityDefaults{
Planner: temporal.ActivityTimeoutDefaults{
QueueWaitTimeout: 30 * time.Second,
LivenessTimeout: 20 * time.Second,
},
Tool: temporal.ActivityTimeoutDefaults{
QueueWaitTimeout: 2 * time.Minute,
LivenessTimeout: 20 * time.Second,
},
},
})
if err != nil {
panic(err)
}
Questa separazione tiene la meccanica del workflow dietro il confine di Temporal, mentre il runtime generico resta coerente sia con Temporal sia con il motore in memoria.
Contratti dell’adattatore di storage e completamento
Il runtime registra una sola activity tipizzata chiamata runtime.store. Ogni
StorageActivityCommand imposta esattamente uno tra Append, RootStart,
ChildStart, OneShotStart, OneShotChildStart, Cancellation, Suspension
e Terminal. Il
StorageActivityResult restituito imposta esattamente il campo corrispondente e
nessun altro. Uno storage personalizzato restituisce storage.ContractError
quando ripetere lo stesso comando non può riuscire. Gli errori temporanei del
database o della rete restano errori normali e possono essere ritentati.
runtime.WithStorageActivityTimeout imposta il timeout Start-to-Close
dell’activity e richiede un valore maggiore di zero.
Engine.QueryRunCompletion restituisce lo Status corrente dell’esecuzione.
Dopo la chiusura dell’esecuzione, lo stesso risultato contiene anche l’istante
stabile CompletedAt e l’Output finale o il WorkflowError.
EnsureRunCompletion usa CompletedAt come timestamp del record, quindi ogni
nuovo tentativo invia lo stesso valore. L’errore separato del metodo indica che
il motore non ha potuto recuperare queste informazioni. Non esiste una query
separata per lo stato.
La preparazione del prompt di un figlio restituisce esattamente un Success o
un Failure. Il successo contiene soltanto i messaggi e i dati dei prompt
renderizzati. Il workflow ricava l’identità dell’esecuzione figlia, della
sessione, del padre, dello strumento e delle etichette dalla chiamata originale
già registrata. Il motore in memoria copia e limita input e output e applica la
stessa politica di retry di Temporal.
Contratti di esecuzione
SessionIDè richiesto per gli avvii con sessione.StarteRunfalliscono rapidamente quandoSessionIDè vuoto o contiene solo spaziStartOneShoteOneShotRunsono esplicitamente senza sessione. Non richiedono né creano una sessione e non emettono eventi di stream con scope di sessione- L’host crea le sessioni prima di inviare lavoro con sessione. I runtime degli agenti non creano, terminano o eliminano sessioni
- Il motore accetta un workflow radice prima che la prima activity salvi l’esecuzione. Il runtime non crea alcun record
pendingprima dell’accettazione - Ripetere un avvio con lo stesso ID di esecuzione e la stessa richiesta restituisce il workflow accettato finché la sua cronologia resta interrogabile. Riutilizzare l’ID con input diverso viene rifiutato. Dopo la conservazione della cronologia, l’identità permanente del comando appartiene al servizio del prodotto, non a Goa-AI
- Gli avvii radice, figlio e one-shot usano operazioni distinte. Gli avvii dei figli salvano insieme il collegamento al padre e l’avvio del figlio; gli avvii one-shot salvano metadati completi senza sessione
- Temporal termina un workflow figlio se il workflow padre si chiude per primo
- Un nuovo figlio richiede un padre attivo.
StartChildRuneStartOneShotChildRunsalvano ciascuno il collegamento al padre e l’avvio del figlio insieme. Un retry identico già accettato resta valido dopo l’arresto del padre; un retry modificato o un nuovo figlio vengono rifiutati - Il primo motivo di annullamento non cambia. Un retry esatto riesce e un motivo diverso per la stessa esecuzione produce un conflitto
- La sospensione e il completamento salvano il nuovo stato insieme al record corrispondente, che non può più essere modificato
- I payload persistenti di
RunStarted,RunSuspended,RunCompletedeChildRunLinkeddevono contenere esattamente un valore JSON del tipo corrispondente. I campi sconosciuti e gli ulteriori valori JSON vengono rifiutati - Gli agenti devono essere registrati prima della prima esecuzione. Il runtime rifiuta la registrazione dopo l’invio della prima esecuzione con
ErrRegistrationClosedper mantenere i lavoratori del motore deterministici - Gli esecutori degli strumenti ricevono metadati espliciti per chiamata (
ToolCallMeta) piuttosto che pescare valori dacontext.Context - Non fare affidamento su fallback impliciti; tutti gli identificatori di dominio (esecuzione, sessione, turno, correlazione) devono essere passati esplicitamente
Garantire il record finale e la sua consegna
I workflow normali ritentano le scritture di sospensione e completamento finché lo storage del runtime non le accetta. Un host può usare due comandi espliciti dopo la chiusura della cronologia del motore:
Runtime.EnsureRunCompletion(ctx, runID)salva una sospensione o un risultato finale mancante mentre l’esecuzione è ancora attiva nello storage. Se è già terminata, o se un altro risultato finale prevale durante il comando, convalida e consegna esattamente il risultato salvato.Runtime.EnsureChildRunLink(ctx, runID)convalida e consegna soltanto l’esatto collegamento al padre di un’esecuzione figlia associata a una sessione. Gli host possono chiamarlo dal padre verso i figli prima di consegnare i risultati finali dei figli annidati.
EnsureRunCompletion consegna il collegamento al padre prima dell’evento finale
di un figlio. Le chiavi evento stabili rendono sicura la consegna ripetuta allo
stream e un risultato già salvato non produce un’altra notifica locale del
ciclo di vita. Nessuno dei due comandi cambia il risultato accettato dallo
storage.
Entrambi i comandi richiedono Runtime.WithStream quando lo stato della sessione
usato per la consegna è attivo. EnsureChildRunLink legge lo stato corrente con
LoadSessionStatus. EnsureRunCompletion usa invece il SessionStatus
restituito insieme alla scrittura del record finale o al suo tentativo identico.
Una sessione appena rilevata come terminata conserva i propri record e sopprime
la consegna allo stream. Se lo storage ha accettato l’evento mentre la sessione
era attiva, l’evento resta da consegnare: terminare la sessione durante i
tentativi di quella stessa chiamata di consegna non lo annulla.
EnsureRunCompletion restituisce ErrRunCompletionNotReady quando il motore
segnala ancora un workflow attivo. Restituisce ErrRunCompletionCorrupt quando
la cronologia del motore o i dati del ciclo di vita salvati non possono formare
un unico risultato valido. Un errore nel caricamento della cronologia del
motore viene restituito al chiamante e non viene mai salvato come errore del
workflow.
I metodi di elenco e snapshot sono di sola lettura e non chiamano mai questi comandi. I comandi non aggiungono una migrazione dello schema del database e non cambiano alcun formato pubblico. Cambiano però l’interfaccia Go degli storage personalizzati, e i record persistenti esistenti devono rispettare le forme JSON tipizzate e rigorose descritte in Memoria e sessioni.
Input esterno e continuazioni dei workflow
Ogni input utente accettato avvia un solo workflow principale per quel turno. Il workflow termina con il risultato finale del turno oppure con una sospensione per input esterno. Gli agenti annidati continuano a essere workflow figli collegati.
Chiarimenti, domande strutturate, risultati di strumenti esterni e conferme
concludono con successo il workflow corrente. Il RunOutput.Suspension
restituito contiene richieste Pending visibili e un Checkpoint privato.
L’applicazione conserva la sospensione completa in uno storage server affidabile
e invia solo Suspension.Pending alla UI o al sistema esterno che deve
rispondere. Non invia mai il checkpoint privato a un client non attendibile.
Nessun workflow Temporal resta aperto mentre una persona decide.
Prima di terminare, Goa-AI salva il checkpoint privato sotto l’ID del run completato. L’applicazione deve accettare atomicamente una sola risposta, quindi avvia un nuovo workflow con l’ID del run precedente, un nuovo run ID, un nuovo turn ID e una sola risposta tipizzata:
Se l’accettazione della risposta deve essere salvata insieme a dati del
prodotto, chiamare PrepareContinuation, quindi MarshalBinary, e salvare quei
byte con la risposta in un’unica transazione. Il processo che avvia il workflow
carica i byte, chiama ParsePreparedRun e passa il valore ripristinato a
StartPrepared. Usare Continue solo quando non vi è una scrittura
applicativa tra convalida e invio al motore.
next, err := client.Continue(
ctx,
"session-1",
previous.RunID,
"run-124",
"turn-2",
&api.PendingInputResponse{
Clarification: &api.ClarificationAnswer{
ID: "clarify-device",
Answer: "Device ID is ABC-123",
},
},
runtime.WorkflowOptions{},
)
Durante la preparazione della continuazione, l’applicazione passa soltanto l’ID
dell’esecuzione completata e la risposta tipizzata. Goa-AI carica il checkpoint,
ne convalida la versione e la richiesta in attesa, ripristina i payload salvati
con i codec generati correnti e riprende la pianificazione. I byte di
PreparedRun possono contenere una copia del checkpoint e la trascrizione
completa. Salvali solo in uno storage applicativo affidabile e con accesso
controllato; non inviarli mai a un client non attendibile.
L’unico formato accettato è goa-ai.run-suspension.v8. La versione otto salva i
nomi degli strumenti annunciati quando un piano di recupero accettato attende
un input: i nomi degli strumenti falliti non bastano a ricostruire le altre
scelte offerte in quel turno. La continuazione conserva queste scelte e
verifica comunque la definizione attuale dell’agente e la policy di esecuzione.
Goa-AI rifiuta tutte le versioni precedenti del checkpoint. Prima dell’upgrade, completare il lavoro salvato nel vecchio formato con il runtime che lo possiede. Se deve restare lavoro incompiuto, l’host deve decidere esplicitamente come conservarlo e se resterà riprendibile; questo runtime non può riprenderlo. Il framework non fornisce comandi di conversione e non elimina, annulla o riscrive automaticamente il lavoro salvato.
Quando una risposta completa una chiamata a un tool creata dal modello nel
workflow precedente, il nuovo evento tool_end contiene due identità:
- il normale ID di esecuzione identifica il nuovo workflow che ha ricevuto la risposta;
call_run_ididentifica il workflow precedente che ha emessotool_start.
I consumer dello stream devono associare questi eventi con call_run_id e l’ID
della chiamata. Non devono cercare esecuzioni precedenti né supporre che la
chiamata e il risultato appartengano allo stesso workflow.
Conferma dello strumento
Goa-AI supporta gate di conferma forzati a tempo di esecuzione per gli strumenti sensibili (scritture, cancellazioni, comandi).
È possibile abilitare la conferma in due modi:
- Design-time (caso comune): dichiarare
Confirmation(...)all’interno del DSL dello strumento. Codegen memorizza il criterio intools.ToolSpec.Confirmation. - Runtime (sovrascrittura/dinamica): passare
runtime.WithToolConfirmation(...)quando si costruisce il runtime per richiedere la conferma di strumenti aggiuntivi o per sovrascrivere il comportamento in fase di progettazione.
Durante l’esecuzione, il workflow emette una richiesta di conferma e termina con una sospensione. La decisione accettata avvia un nuovo workflow. La continuazione esegue lo strumento soltanto se approvato; in caso di rifiuto il runtime sintetizza un risultato conforme allo schema, così trascrizione e planner restano deterministici.
Protocollo di conferma
In fase di runtime, la conferma è implementata come un protocollo di attesa/decisione dedicato:
Carico di attesa (trasmesso come
await_confirmation):{ "id": "...", "title": "...", "prompt": "...", "tool_name": "facility.commands.change_setpoint", "tool_call_id": "toolcall-1", "payload": { "...": "canonical tool arguments (JSON)" } }
Contratto:
payloadcontiene sempre gli argomenti JSON canonici del tool per la chiamata in attesa. Se la chiamata viene approvata, sono questi gli argomenti che il runtime esegue.Le override di conferma possono personalizzare il prompt e il rendering del risultato negato, ma non introducono un canale separato di display payload e non cambiano il significato di
payload.I prodotti che hanno bisogno di una UI di conferma più ricca devono materializzarla nel layer applicativo a partire dal payload canonico e da letture possedute dall’applicazione.
Risposta di continuazione:
response := &api.PendingInputResponse{ Confirmation: &api.ConfirmationDecision{ ID: "await-1", Approved: true, // or false RequestedBy: "user:123", Labels: map[string]string{"source": "front-ui"}, Metadata: map[string]any{"ticket_id": "INC-42"}, }, }
Eventi di autorizzazione dello strumento
Quando viene fornita una decisione, il runtime emette un evento di autorizzazione di primo ordine:
- Hook event:
hooks.ToolAuthorization - Stream event type:
tool_authorization
Questo evento è il record canonico “chi/quando/cosa” per una chiamata tool confermata:
tool_name,tool_call_idapproved(true/false)summary(riepilogo deterministico renderizzato dal runtime)approved_by(copiato daapi.ConfirmationDecision.RequestedBy, identificatore di principal stabile)
L’evento viene emesso immediatamente dopo la ricezione della decisione (prima dell’esecuzione del tool se approvato e prima della sintesi del risultato negato se rifiutato).
Note:
- I consumatori devono trattare la conferma come un protocollo di runtime:
- Visualizzare il primo elemento in attesa quando il suo tipo è
confirmation, quindi inviare la decisione conAgentClient.Continue. - Non associare il comportamento dell’interfaccia utente a un nome specifico di strumento di conferma; trattarlo come un dettaglio di trasporto interno.
- Visualizzare il primo elemento in attesa quando il suo tipo è
- I modelli di conferma (
PromptTemplateeDeniedResultTemplate) sono stringhe Gotext/templateeseguite conmissingkey=error. Oltre alle funzioni standard dei template (ad esempioprintf), Goa-AI fornisce:json v→ codifica JSONv(utile per i campi opzionali dei puntatori o per incorporare valori strutturati).quote s→ restituisce una stringa quotata Go-escaped (comefmt.Sprintf("%q", s)).
Convalida in fase di esecuzione
Il runtime convalida le interazioni di conferma al confine:
- La conferma
IDcorrisponde all’identificatore dell’elemento in attesa. - La continuazione contiene esattamente una variante di risposta e una decisione ben formata.
Contratto del pianificatore
I pianificatori attuano:
type Planner interface {
PlanStart(ctx context.Context, input *planner.PlanInput) (*planner.PlanResult, error)
PlanResume(ctx context.Context, input *planner.PlanResumeInput) (*planner.PlanResult, error)
}
PlanResult contiene chiamate agli strumenti, risposta finale, risultato finale
dello strumento, annotazioni e la transizione selezionata dopo gli strumenti.
PlanResumeInput indica al planner perché viene chiamato.
Le richieste create dal planner contengono soltanto l’intento di dominio. Usare
planner.NewToolRequest(typedTool, payload) per codificarne una. Quando si
inoltra una chiamata validata del provider, usare
planner.ToolRequestFromModelCall(call): conserva l’ID di correlazione del
provider senza trasformarlo nell’ID di esecuzione del runtime. Il runtime valida
l’intero piano prima di assegnare gli ID di esecuzione o pubblicare eventi.
Questi contratti sono distinti:
| Contratto | Ambito | Significato |
|---|---|---|
ToolSpec.Tags | Uno strumento, per ogni run | Etichette piatte disponibili al filtro generico di policy e interfaccia utente. |
ToolSpec.Meta | Uno strumento, per ogni run | Annotazioni generate e inerti la cui semantica appartiene al consumer denominato; i metadati da soli non cambiano il runtime. |
ToolSpec.Bookkeeping | Uno strumento, per ogni run | La chiamata è un record di controllo durevole il cui successo non richiede un altro turno del planner. Non consuma budget di retrieval o di errori consecutivi. |
ToolSpec.TerminalRun | Uno strumento, per ogni run | Il successo termina direttamente il run e implica automaticamente bookkeeping. |
ToolFailure.Recovery.Action | Un risultato fallito | Sceglie la correzione mantenendo disponibile lo strumento fallito, una nuova pianificazione senza di esso oppure la finalizzazione. |
PlanResult.SynthesizeAfterTools | Un batch selezionato | Se il batch non contiene errori recuperabili, il turno successivo del planner deve rispondere. |
PlanResumeInput.SynthesisOnly | Un’attività del planner | Restituire una risposta finale; le chiamate agli strumenti non sono valide. |
PlanResumeInput.Finalize | Conclusione imposta dal runtime | Le nuove operazioni sono vietate. Con il motivo tool_failure, il catalogo annunciato può conservare pagine di query già iniziate. |
Il runtime sceglie il prossimo stato in quest’ordine:
| Passo completato | Stato successivo |
|---|---|
| Un limite o una deadline richiede la finalizzazione | Turno Finalize |
Uno strumento TerminalRun è riuscito | Termine immediato |
Resta attivo un errore finish | Finalize con motivo tool_failure; conserva le pagine annunciate e gli strumenti terminali di bookkeeping |
Un risultato fallito ha AllowsToolTurn() == true | Normale turno di riparazione |
SynthesizeAfterTools è true | Turno SynthesisOnly |
| Altrimenti | Normale turno di continuazione |
In questo modo l’intento del planner non diventa una seconda policy di retry.
Un errore recuperabile viene riparato per primo; un batch finale riuscito o con
errore terminale passa alla sintesi. Il runtime rifiuta chiamate agli strumenti
restituite da un turno SynthesisOnly.
Recupero dopo un errore dello strumento
Ogni ToolFailure recuperabile seleziona anche una Recovery.Action:
correct_callmantiene disponibile lo strumento che ha fallito e fornisce al turno successivo del planner l’input rifiutato, i problemi di validazione generati, le indicazioni sui campi e un esempio. Non richiede una chiamata sostitutiva per ogni errore. Il planner può combinare il lavoro, effettuare un numero qualsiasi di chiamate valide agli strumenti annunciati, attendere un input o rispondere usando le prove già raccolte.replanrimuove lo strumento che ha fallito dal turno successivo. Il planner può usare un altro strumento annunciato, attendere un input o rispondere.finishvieta nuove operazioni fino al termine del run. Il planner può rispondere, usare gli strumenti terminali registrati per salvare il risultato finale oppure leggere una pagina annunciata di una query già iniziata.
Con finish, PlanResumeInput.Finalize porta il motivo tool_failure. Il
catalogo corrente, non il solo motivo, determina le azioni disponibili. Una
pagina e un invio terminale non possono condividere un batch: il runtime lo
rifiuta prima di eseguire qualsiasi chiamata. Rifiuta anche nuove richieste di
input e passaggi separati di sintesi. Senza pagine disponibili resta solo la
conclusione terminale. La finalizzazione dovuta a scadenze o limiti non consente mai la paginazione.
Gli errori attivi degli strumenti e le indicazioni su una risposta del modello
rifiutata sono fatti separati. I rifiuti e le pagine ottenute conservano
l’errore originale, il suo messaggio e il divieto di nuovo lavoro. Le normali
restrizioni correct_call e replan terminano con il proprio episodio di
recupero. La validazione del modello fornisce vincoli dello schema ed esempi;
il runtime richiede una risposta sostitutiva conforme alle azioni e ai requisiti
di conclusione correnti, non necessariamente un’altra chiamata a uno strumento.
Le pagine riuscite consumano i normali budget di strumenti e tempo, ma non
un tentativo sostitutivo e non ripristinano la disponibilità dell’errore
finish attivo. Ogni sostituzione causata da un rifiuto consuma ancora
MaxRecoveryTurns. Se l’invio terminale richiede argomenti corretti, la richiesta
successiva conserva l’errore originale e la nuova diagnosi di validazione. Per
quella correzione viene annunciato solo lo strumento terminale fallito; le pagine non ricompaiono.
Un normale turno correct_call combina gli strumenti eseguibili dell’agente
attuale con i contratti esatti degli strumenti falliti. Nomi e contratti
identici vengono deduplicati; contratti in conflitto, registrazioni di
esecuzione mancanti e strumenti revocati causano un errore prima della chiamata
al modello. Restano applicabili le restrizioni del chiamante, quelle dei tag
del run e le esclusioni di recupero. Uno strumento di correzione negato causa
un errore; il runtime non lo scarta silenziosamente e non lo ripristina dopo il
filtro. L’autorizzazione nel servizio esecutore verifica ancora ogni chiamata.
Le query incompiute conservano le azioni di continuazione generate dal runtime;
le richieste fallite non creano continuazioni. La finalizzazione forzata offre
per la correzione solo l’esatto strumento terminale fallito, e i turni di sola
sintesi restano privi di strumenti. Dopo una correzione ordinaria, i turni
normali tornano agli strumenti dell’agente attuale; un errore finish attivo
continua a vietare nuove operazioni.
Il workflow possiede le prove di correzione mostrate al modello. Prima di
salvare l’errore nella cronologia, sostituisce input ed esempi forniti
dall’executor con la chiamata originale del provider e la specifica registrata.
Una continuazione creata dal runtime non ha input prodotto dal modello e non
può richiedere correct_call: in questo modo cursor privati e campi iniettati
non entrano in richieste successive. Le trascrizioni del modello correlano i
risultati con ModelToolCallID; attività, retry e record di esecuzione usano il
distinto ToolCallID del runtime.
Il runtime registra il catalogo esatto mostrato durante un turno di recupero e
rifiuta ogni chiamata eseguibile che non ne faccia parte, incluse le chiamate
incorporate in una richiesta di input utente o esterno. I codec generati
continuano a validare ogni payload e i limiti di strumenti, errori e tempo del
run continuano a interrompere il lavoro non valido ripetuto. Se un turno di
recupero attende un input, le prove dell’errore restano disponibili alla
ripresa. Il lavoro di recupero ordinario accettato termina il suo episodio;
una pagina o una risposta sostitutiva non elimina mai un errore finish attivo.
Gli input delle attività di recupero e il catalogo annunciato fanno parte della cronologia durevole del workflow. I deployment di produzione devono usare Temporal Worker Deployment Versioning con versioni fissate e conservare ogni vecchia versione finché Temporal non la segnala come drenata. Avviare un nuovo worker non autorizza il replay di un workflow esistente con nuovo codice. Una continuazione è un nuovo workflow e può usare la versione corrente dopo la validazione del checkpoint salvato.
Questo cambiamento non aggiunge campi ad attività o checkpoint e non richiede
migrazioni dei dati. I planner personalizzati devono usare il catalogo
annunciato insieme a Finalize.Reason; aggiornarli con il runtime. Le vecchie
cronologie che hanno riaperto lavoro di dominio dopo un errore finish non
sono compatibili con questa esecuzione: i loro piani con strumenti ordinari
vengono rifiutati prima dell’esecuzione. Conservare le cronologie interessate
non ancora terminate sulla versione di worker originale oppure completarle
prima di sostituirla. Decodificare un checkpoint non dimostra compatibilità
di replay della cronologia. Verificare l’assegnazione delle cronologie ai worker
anche per il rollback; non mescolare versioni delle attività in un workflow interessato.
Quando PlanResumeInput.Finalize è impostato, i planner possono restituire
strumenti terminali di bookkeeping; queste chiamate non vengono riprodotte in
un turno successivo e devono completare durevolmente la finalizzazione.
I pianificatori ricevono anche un PlannerContext tramite input.Agent che espone i servizi del runtime:
AdvertisedToolDefinitions()- ottenere le definizioni degli strumenti filtrate dal runtime e visibili al modello in questo turnoModelClient(id string)- ottenere un client di modello grezzo indipendente dal providerPlannerModelClient(id string)- ottenere un client di modello con ambito planner e emissione degli eventi gestita dal runtimeRenderPrompt(ctx, id, data)- risolvere e renderizzare il contenuto prompt per lo scope corrente della runAddReminder(r reminder.Reminder)- registrare promemoria di sistema di runscopeRemoveReminder(id string)- cancellare i promemoria quando le precondizioni non sono più valideMemory()- accedere alla cronologia delle conversazioni
Preparare i messaggi della conversazione
PlanInput e PlanResumeInput forniscono la funzione obbligatoria
PrepareMessages func() ([]*model.Message, error), da chiamare prima di leggere
o trasformare la cronologia, anche per una decisione senza modello. La prima
chiamata applica la policy della cronologia ai messaggi dell’attività e agli
strumenti pubblicizzati, con il contesto, la scadenza e l’annullamento di quella
attività. Una decisione basata solo su RunContext, ToolOutputs o Finalize
può non chiamarla: non vengono eseguiti conteggi o riepiloghi inutilizzati.
Le chiamate ripetute o concorrenti restituiscono la stessa slice di messaggi e lo stesso errore, senza ripetere la policy né copiare i messaggi. Il pianificatore resta responsabile di sincronizzare le modifiche concorrenti. Tutte le chiamate devono terminare prima del suo ritorno; non conservare la funzione per usarla in seguito. Il runtime la fornisce anche per una cronologia vuota o senza policy configurata. Non esiste un accesso alternativo alla cronologia non preparata.
Restituisci subito l’errore di preparazione. Anche se il pianificatore lo ignora, il runtime rifiuta il risultato e fa fallire l’attività con quell’errore, conservandone messaggio completo, cause e classificazione. Un successivo errore del pianificatore o del modello non lo sostituisce. La preparazione non viene ritentata nella stessa attività; un nuovo tentativo dell’attività riceve una nuova funzione. Il replay di un’attività completata usa il risultato registrato, senza nuova preparazione.
Migrazione: sostituisci i vecchi campi PlanInput.Messages e
PlanResumeInput.Messages con la chiamata a PrepareMessages e gestisci l’errore.
Anche i test diretti devono fornire la funzione; inoltrala invariata quando
converti un input del pianificatore in un altro. Aggiorna i chiamanti insieme
alla dipendenza goa-ai. RunInput.Messages, richieste del modello, dati delle
attività e trascrizioni salvate restano invariati: cambia solo l’API Go, senza
migrazione di dati o protocolli. Limiti, firme del ragionamento, coppie
chiamata/risultato e streaming non cambiano; i riepiloghi non vengono riutilizzati
tra attività.
Moduli funzionali
runtime/agent/storage/inmem– archivio integrato in memoria per esempi e testruntime/mcp- chiamanti MCP per HTTP e stdio; HTTP accetta risposte JSON e flussi di eventifeatures/memory/mongo- archivio di memoria durevolefeatures/prompt/mongo- prompt store Mongo per override dei promptfeatures/stream/pulse- Aiutanti di Pulse sink/subscriberfeatures/model/{anthropic,bedrock,openai}- adattatori client di modelli per pianificatorifeatures/model/middleware- middleware condivisimodel.Client(ad esempio, limitazione della velocità adattiva)features/policy/basic- semplice motore di policy con elenchi allow/block e gestione diToolFailure
Modellare il throughput del cliente e il rate limiting
Goa-AI fornisce un limitatore di velocità adattivo indipendente dal provider in
features/model/middleware. Avvolge qualsiasi model.Client, richiede il
conteggio esatto dei token di input, accoda i chiamanti e regola il budget
effettivo di token al minuto tramite AIMD quando il provider segnala il
throttling. Non stima token e non misura le quote di output.
import (
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"goa.design/goa-ai/runtime/agent/runtime"
"goa.design/goa-ai/features/model/bedrock"
mdlmw "goa.design/goa-ai/features/model/middleware"
)
awsClient := bedrockruntime.NewFromConfig(cfg)
bed, err := bedrock.New(awsClient, bedrock.Options{
DefaultModel: "us.anthropic.claude-4-5-sonnet-20251120-v1:0",
})
if err != nil {
panic(err)
}
rl := mdlmw.NewAdaptiveRateLimiter(
ctx,
throughputMap, // *rmap.Map joined earlier (nil for process-local)
"bedrock:sonnet", // key for this model family
80_000, // initial TPM
1_000_000, // max TPM
)
limited, err := rl.Middleware()(bed)
if err != nil {
panic(err)
}
rt := runtime.New(runtimeStore)
if err := rt.RegisterModel("bedrock", limited); err != nil {
panic(err)
}
La costruzione del middleware non verifica il supporto del conteggio. Se il
provider o la richiesta non possono essere contati esattamente, la prima
chiamata Complete o Stream restituisce
model.ErrTokenCountingUnsupported prima dell’inferenza. Vertex Gemini
supporta il conteggio esatto; Bedrock supporta solo le richieste e i modelli
accettati da Runtime CountTokens, mentre OpenAI non ha un contatore nativo.
Integrazione LLM
I pianificatori Goa-AI interagiscono con i modelli linguistici di grandi dimensioni attraverso un’interfaccia agnostica rispetto ai provider. Questo design consente di cambiare i provider - Bedrock di AWS, OpenAI, Google Vertex AI (Gemini e Claude-on-Vertex) o endpoint personalizzati - senza modificare il codice del pianificatore.
Il client del modello validato
Tutte le interazioni del planner passano attraverso un model.Client opaco:
resp, err := client.Complete(ctx, req)
stream, err := client.Stream(ctx, req) // *model.ValidatedStream
Le integrazioni implementano model.Provider, che produce risposte e chunk
grezzi del trasporto. Goa-AI costruisce model.Client con
model.NewClient(provider) e valida richieste e risposte complete attorno al
provider. I package esterni non possono implementare model.Client né esporre
chunk grezzi al planner.
Prima della chiamata, il client valida nomi e schemi degli strumenti, parti dei messaggi, opzioni di thinking, metadati dello structured output e valori dinamici. Richieste e risposte unary sono limitate a 16 MiB e 100.000 valori visitati; i metadati annidati hanno profondità massima 64. Lo streaming applica un solo budget cumulativo ai chunk e alla risposta terminale. Questi limiti rifiutano l’intera operazione: Goa-AI non tronca, ripara o converte i dati del modello.
ValidatedStream deve essere consumato fino a io.EOF; solo allora
Response() restituisce la risposta canonica accettata. Uno stream incompleto,
malformato o contraddittorio restituisce un errore e nessuna risposta accettata.
Le chiamate complete agli strumenti devono rispettare lo schema annunciato e il
decoder generato associato, se presente, prima di essere visibili al planner.
I metadati permettono di spiegare violazioni indipendenti di campi obbligatori,
tipi, enumerazioni o lunghezze degli array. Il limite di un array è inclusivo e
riguarda quell’array inviato, non l’intera esecuzione:
Field "items" must contain at most 3 items.
Il runtime segue vincoli obbligatori indipendenti, incluso allOf, e solo il
ramo dell’unione selezionato da un discriminatore valido. Non trasforma rami
alternativi di anyOf, candidati di contains o controlli sui nomi delle
proprietà in obblighi di modifica di ogni valore corrispondente. Indici degli
array e chiavi delle mappe appaiono come *; i campi non dichiarati sono segnalati
sull’oggetto padre senza ripetere il nome inviato.
Le istruzioni sono ordinate e deduplicate. Istruzioni distinte per lo stesso percorso visualizzato vengono omesse, senza tentare di combinarne i vincoli. Le istruzioni utili per altri campi restano disponibili anche quando altre sono ambigue, non supportate o troppo grandi. Ogni istruzione, incluse descrizione ed enumerazioni, rientra interamente nel limite di byte; una correzione parziale segnala che altri errori non sono descritti. Se non è possibile includerne alcuna, la guida resta generica. Gli argomenti restano rifiutati prima dell’esecuzione; Goa-AI non li divide, tronca o riscrive. Errori originali, regole di accettazione e limite dei turni di recupero restano invariati.
Per i rifiuti dello schema o della validazione tipizzata degli argomenti che ammettono una correzione, il client del modello può aggiungere l’esempio di input completo e validato dopo l’indicazione sul campo. Copia l’esempio insieme al contratto di validazione della richiesta prima di chiamare il modello; una successiva modifica della richiesta non può sostituirlo. L’istruzione allegata chiede al modello di scegliere valori e un ramo valido dell’unione adatti alla richiesta, anziché copiare i valori di esempio.
La correzione completa deve rientrare nel limite esistente di 4.096 byte per ogni invocazione del modello rifiutata, includendo istruzione, esempio e byte UTF-8. Se l’esempio manca o è troppo grande, l’indicazione sul campo resta invariata; l’esempio viene omesso per intero, mai troncato. Il limite riguarda il contesto facoltativo della correzione, non gli argomenti. Validazione, diagnostica delle risposte rifiutate, cronologia accettata, strumenti disponibili e limite dei turni di recupero non cambiano; gli argomenti non vengono mai riparati e non si aggiunge alcun tentativo. Vedere il contratto del framework.
Adattatori del provider
Goa-AI viene fornito con adattatori per i più diffusi provider LLM:
AWS Bedrock
import (
"github.com/aws/aws-sdk-go-v2/service/bedrockruntime"
"goa.design/goa-ai/features/model/bedrock"
)
awsClient := bedrockruntime.NewFromConfig(cfg)
modelClient, err := bedrock.New(awsClient, bedrock.Options{
DefaultModel: "anthropic.claude-3-5-sonnet-20241022-v2:0",
HighModel: "anthropic.claude-sonnet-4-20250514-v1:0",
SmallModel: "anthropic.claude-3-5-haiku-20241022-v1:0",
MaxTokens: 4096,
Temperature: 0.7,
})
OpenAI
import (
"os"
"goa.design/goa-ai/runtime/agent/runtime"
)
rt := runtime.New(runtimeStore) // storage del runtime fornito dall'host
modelClient, err := rt.NewOpenAIModelClient(runtime.OpenAIConfig{
APIKey: os.Getenv("OPENAI_API_KEY"),
DefaultModel: "gpt-5-mini",
HighModel: "gpt-5",
SmallModel: "gpt-5-nano",
})
if err != nil {
panic(err)
}
Google Vertex AI (Gemini e Claude-on-Vertex)
Il pacchetto features/model/vertex fornisce due costruttori che soddisfano
entrambi model.Client: un adattatore Gemini nativo e un helper di pura
costruzione che punta l’adattatore Anthropic ai modelli Claude ospitati su
Vertex.
import "goa.design/goa-ai/runtime/agent/runtime"
// Gemini su Vertex, con Application Default Credentials.
geminiClient, err := rt.NewVertexGeminiModelClient(ctx, runtime.VertexConfig{
ProjectID: "my-gcp-project",
Location: "us-central1",
DefaultModel: "gemini-2.5-flash",
HighModel: "gemini-3-pro-preview",
SmallModel: "gemini-2.5-flash-lite",
MaxTokens: 4096,
ThinkingBudget: 10000,
})
// Claude su Vertex. Questa è pura costruzione: crea un client Anthropic SDK
// sul trasporto Vertex dell'SDK e lo passa a features/model/anthropic, che
// possiede la traduzione dei Messages e la classificazione degli errori
// HTTP per ogni adattatore ospitato da Anthropic (API diretta e Vertex)
// — nessun livello di traduzione separato.
claudeOnVertexClient, err := rt.NewVertexAnthropicModelClient(ctx, runtime.VertexConfig{
ProjectID: "my-gcp-project",
Location: "us-east5",
DefaultModel: "claude-sonnet-4-5@20250929",
})
I modelli di classe Gemini 3 allegano una thought signature opaca alle
parti functionCall (non solo alle parti thought/thinking) per autenticare la
catena di ragionamento dietro una chiamata a strumento. L’adattatore Vertex fa
il round-trip di questa firma tramite model.ToolCall.ThoughtSignature /
model.ToolUsePart.ThoughtSignature usando la stessa convenzione base64 di
ThinkingPart.Signature. Il runtime cattura questa firma al confine del
model-client — prima che uno dei due stili di integrazione qui sotto produca
mai un planner.ToolRequest — e la ricollega tramite l’ID della chiamata a
strumento quando ricostruisce il transcript per il provider.
planner.ToolRequest non ha alcun campo firma; il codice del pianificatore
non ha bisogno di sapere che le firme esistono.
Gli errori sentinella comuni includono
model.ErrStructuredOutputUnsupported,
model.ErrTokenCountingUnsupported, model.ErrEmptyStream e
model.ErrRateLimited. *planner.OutputContractError è invece un errore
strutturato: rilevarlo con errors.As e ispezionarne l’origine per distinguere
output non valido del modello, del planner o dello strumento. Non è riprovabile,
perché una nuova richiesta non deve nascondere una violazione del contratto.
Metadati canonici e replay delle citazioni
model.Message.Meta contiene i dati prodotti dal provider necessari per
riprodurre esattamente una risposta. I confini che persistono o trasportano i
metadati devono usare model.MarshalMetadata e model.UnmarshalMetadata.
Questi codec richiedono un singolo oggetto JSON, conservano i numeri decodificati
come json.Number, rifiutano dati successivi e canonicalizzano nil o un oggetto
vuoto a nil.
Il replay delle citazioni è specifico del provider e non deve mai appiattire le
citazioni in testo ordinario. L’adattatore Bedrock può riprodurre i valori
CitationsPart dell’assistente come blocchi di citazione nativi, preservando
l’identità della fonte, gli estratti e le posizioni nel documento per caratteri,
chunk o pagine. Le citazioni di sistema Bedrock restano non supportate perché
la relativa unione di contenuto non prevede citazioni. Anthropic e Vertex
rifiutano il replay quando la parte canonica non contiene i campi richiesti dal
protocollo del provider.
Utilizzo dei client modello nei pianificatori
I pianificatori ottengono i client di modello tramite il PlannerContext del
runtime. Ora esistono due stili di integrazione espliciti:
PlannerModelClient(id)per lo streaming con ambito planner e l’emissione degli eventi gestita dal runtimeModelClient(id)quando serve accesso grezzo al trasporto e lo si abbinerà aplanner.ConsumeStreamoppure si emetterannoPlannerEventsmanualmente
PlannerModelClient (Consigliato)
PlannerContext.PlannerModelClient(id) restituisce un client con ambito planner
che si occupa dell’emissione di AssistantChunk, PlannerThinkingBlock e
UsageDelta. Il suo metodo Stream(...) drena il flusso del provider
sottostante e restituisce un planner.StreamSummary:
func (p *MyPlanner) PlanStart(ctx context.Context, input *planner.PlanInput) (*planner.PlanResult, error) {
messages, err := input.PrepareMessages()
if err != nil {
return nil, err
}
mc, ok := input.Agent.PlannerModelClient("anthropic.claude-3-5-sonnet-20241022-v2:0")
if !ok {
return nil, errors.New("model not configured")
}
req := &model.Request{
Messages: messages,
Tools: input.Agent.AdvertisedToolDefinitions(),
Stream: true,
}
sum, err := mc.Stream(ctx, req)
if err != nil {
return nil, err
}
if len(sum.ToolCalls) > 0 {
return &planner.PlanResult{ToolCalls: sum.ToolCalls}, nil
}
final := sum.FinalResponse()
if final == nil {
return nil, errors.New("model stream ended without a canonical response")
}
return &planner.PlanResult{
FinalResponse: final,
Streamed: true, // Il testo dell'assistente è già stato trasmesso
}, nil
}
Questo è lo stile più semplice perché il client con ambito planner consuma e
riassume direttamente lo stream validato. Restituire sum.FinalResponse()
seleziona inoltre la risposta esatta del provider catturata per
quell’invocazione; ricostruire un messaggio di solo testo eliminerebbe
ragionamento, citazioni, firme, metadati e confini dei messaggi.
Client grezzo + ConsumeStream
Quando serve il model.Client grezzo, recuperarlo tramite
PlannerContext.ModelClient e abbinarlo a planner.ConsumeStream:
messages, err := input.PrepareMessages()
if err != nil {
return nil, err
}
mc, ok := input.Agent.ModelClient("anthropic.claude-3-5-sonnet-20241022-v2:0")
if !ok {
return nil, errors.New("model not configured")
}
req := &model.Request{
Messages: messages,
Tools: input.Agent.AdvertisedToolDefinitions(),
Stream: true,
}
stream, err := mc.Stream(ctx, req)
if err != nil {
return nil, err
}
sum, err := planner.ConsumeStream(ctx, stream)
if err != nil {
return nil, err
}
if len(sum.ToolCalls) > 0 {
return &planner.PlanResult{ToolCalls: sum.ToolCalls}, nil
}
final := sum.FinalResponse()
if final == nil {
return nil, errors.New("model stream ended without a canonical response")
}
return &planner.PlanResult{
FinalResponse: final,
Streamed: true,
}, nil
Questo helper si limita a consumare lo stream e restituisce uno
StreamSummary; il journal delle invocazioni pubblica successivamente gli
eventi di presentazione e utilizzo accettati.
Usare il client diretto quando il planner deve ispezionare chunk di anteprima
validati o effettuare più chiamate al modello nello stesso turno. Consumare
ogni stream selezionato fino al risultato terminale: una chiusura anticipata
non produce una risposta accettata. Non mescolare
PlannerModelClient.Stream(...) con planner.ConsumeStream; scegliere un solo
proprietario del flusso per turno del planner.
Conservazione esatta e copertura del riepilogo
La policy si applica alla prima chiamata a PrepareMessages, secondo il
contratto di preparazione, non prima di ogni
invocazione del pianificatore.
Con CompressAtMaxInputTokens positivo, un solo riepilogo riceve tutti i turni
precedenti al più recente. Il runtime conta insieme messaggi di sistema,
riepilogo effettivo, turni completi ammissibili e strumenti attuali. Se supera
il limite, rimuove il turno facoltativo più vecchio e riconta fino a trovare
la sequenza finale più lunga che rientra. L’uguaglianza con il limite è ammessa.
Il turno più recente non viene mai riassunto né diviso. KeepMaxTurns e
KeepMaxInputTokens continuano a limitare la conservazione ammissibile; il
riepilogo conta nel limite totale, non nel budget aggiuntivo dei turni precedenti.
Ogni turno rimosso è già stato fornito al modello di riepilogo. Alcuni possono
comparire sia nel riepilogo sia nella cronologia esatta, senza rieseguire gli
strumenti. Questo non prova che il modello interpreti correttamente fatti
ripetuti o contrastanti. Con K turni ammissibili si effettuano al massimo K
conteggi finali, oltre alle verifiche iniziali. Input più ampio e conteggi
aggiuntivi possono aumentare costo e latenza, ma non aggiungono un’altra chiamata
di riepilogo. Se riepilogo e turno più recente non rientrano, oppure un conteggio
o il riepilogo falliscono, la policy restituisce la cronologia originale e
l’errore al runtime, senza riavvio automatico. PrepareMessages espone l’errore
di preparazione, non un accesso alternativo alla cronologia non preparata.
Senza limite totale, si riassume solo il prefisso escluso: i turni conservati
restano invariati, senza sovrapposizione né conteggi finali aggiunti. Con limite
positivo, aggiornare i prompt WithSummaryPrompt che presuppongono «solo la
cronologia scartata» affinché si riferiscano alla cronologia precedente fornita.
Obiettivo scelto, %s, percentuali con escape, modello e ruolo non cambiano;
non servono nuova configurazione o migrazione della cronologia salvata.
Vedere il contratto completo in inglese.
Evidenze fornite al modello di riepilogo
Compress fornisce i messaggi precedenti selezionati come evidenze da riassumere,
non come una conversazione da continuare. Testo, argomenti e risultati completi
degli strumenti, identificatori, stato e testo integrale degli errori e campi
delle citazioni vengono riportati nel formato JSON canonico di model.Message.
Ruoli, posizioni e ordine restano espliciti, senza selezionare, arrotondare o
eliminare valori ripetuti. Il modello decide quali fatti sono rilevanti.
WithSummaryPrompt continua a inserire la trascrizione testuale completa in
%s. Immagini e documenti vengono allegati una sola volta come contenuto nativo
nella stessa chiamata: un messaggio di allegati per ogni messaggio utente
originale contenente media, con riferimenti alle posizioni originali. Non vengono
offerti strumenti da eseguire né estratti o recuperati documenti. Message.Meta,
ragionamento, checkpoint della cache e firme di ragionamento degli strumenti non
vengono copiati nella nuova richiesta. Cronologia originale, messaggi conservati
esattamente, diagnostica ed errori completi rimangono invariati.
Il riepilogo mantiene ruolo e rappresentazione testuale. Conserva nell’ordine le
frasi citate e tutti i campi di attribuzione come record citati. Le coordinate
appartengono alla richiesta che le ha prodotte. Se il nuovo riepilogo contiene
citazioni e ha utilizzato documenti nativi, descrive anche la loro disposizione,
senza corpi dei documenti, riassegnazioni di DocumentIndex o collegamenti inventati.
Copertura e conservazione seguono le regole precedenti; modello, limiti e singola chiamata di riepilogo restano invariati. Contenuti non supportati o richieste di riepilogo troppo grandi producono errori espliciti, senza eliminare evidenze né generare un altro riepilogo. Gli allegati non aggiungono un conteggio separato. Ricevere tutte le evidenze non garantisce che il modello conservi ogni fatto nel testo. Vedere il contratto completo in inglese.
Convalida dell’ordinamento dei messaggi di Bedrock
Quando si usa AWS Bedrock con la modalità di pensiero abilitata, il runtime convalida i vincoli di ordine dei messaggi prima di inviare le richieste. Bedrock richiede:
- Qualsiasi messaggio di assistenza contenente
tool_usedeve iniziare con un blocco di riflessione - Ogni messaggio utente contenente
tool_resultdeve seguire immediatamente un messaggio assistente con blocchi corrispondentitool_use - Il numero di blocchi
tool_resultnon può superare il numero precedentetool_use
Il client Bedrock convalida questi vincoli in anticipo e restituisce un errore descrittivo se violati:
bedrock: invalid message ordering with thinking enabled (run=xxx, model=yyy):
bedrock: assistant message with tool_use must start with thinking
Questa convalida assicura che la ricostruzione del libro mastro trascritto produca sequenze di messaggi conformi al fornitore.
Passi successivi
- Conoscere Toolsets per comprendere i modelli di esecuzione degli strumenti
- Esplorare Agent Composition per i modelli di agenti come strumenti
- Leggere Memoria e sessioni per la persistenza delle trascrizioni