Temps d'exécution
Aperçu de l’architecture
Le moteur d’exécution de Goa-AI orchestre la boucle plan/exécution/reprise, applique les politiques, gère l’état et coordonne avec les moteurs, les planificateurs, les outils, la mémoire, les crochets et les modules de fonctionnalités.
En plus des outils, le runtime prend aussi en charge des contrats
Completion(...) types pour les reponses finales directes de l’assistant.
Ces contrats generent des aides unaires et de streaming sous
gen/<service>/completions. Les noms de completion sont valides par
construction : 1-64 caracteres ASCII, lettres/chiffres/_/- uniquement, et
un debut par lettre ou chiffre. En streaming, completion_delta sert
uniquement d’aperçu et un seul chunk final completion est canonique ; les
fournisseurs sans structured output renvoient model.ErrStructuredOutputUnsupported.
Le schema genere reste canonique et neutre vis-a-vis des fournisseurs ; les
adaptateurs de modele peuvent le normaliser vers un sous-ensemble pris en
charge, mais doivent echouer explicitement lorsqu’ils ne peuvent pas
preserver le contrat declare.
| Couche | Responsabilité |
|---|---|
| DSL + Codegen | Produire des registres d’agents, des spécifications/codecs d’outils, des flux de travail, des adaptateurs MCP.. |
| L’adaptateur de moteur de flux de travail (Workflow Engine Adapter) produit des registres d’agents, des spécifications/codecs d’outils, des flux de travail, des adaptateurs MCP | |
Adaptateur de moteur de flux de travail - L’adaptateur temporel met en œuvre engine.Engine ; d’autres moteurs peuvent s’y greffer | |
| Modules de fonctionnalités | Intégrations optionnelles (MCP, Pulse, magasins Mongo, fournisseurs de modèles) |
Architecture agentique de haut niveau
Au moment de l’exécution, Goa-AI organise votre système autour d’un petit ensemble de constructions composables :
Agents : Orchestrateurs à longue durée de vie identifiés par
agent.Ident(par exemple,service.chat). Chaque agent possède un planificateur, une politique d’exécution, des flux de travail générés et des enregistrements d’outils.Exécutions : Une exécution unique d’un agent. Les exécutions sont identifiées par un
RunIDet suivies viarun.Contextetrun.Handle. Les exécutions sessionnelles sont regroupées parSessionIDetTurnIDpour former des conversations ; les exécutions one-shot sont explicitement sans session.Toolsets & tools : Collections nommées de capacités, identifiées par
tools.Ident(service.toolset.tool). Les ensembles d’outils soutenus par un service appellent des API ; les ensembles d’outils soutenus par un agent exécutent d’autres agents en tant qu’outils.Planificateurs : Votre couche de stratégie basée sur le LLM mettant en œuvre
PlanStart/PlanResume. Les planificateurs décident quand appeler les outils plutôt que de répondre directement ; le moteur d’exécution applique des plafonds et des budgets de temps autour de ces décisions.Arbre d’exécution et agent en tant qu’outil : Lorsqu’un agent appelle un autre agent en tant qu’outil, le runtime démarre un véritable run enfant avec son propre
RunID. LeToolResultparent porte unRunLink(*run.Handle) pointant vers l’enfant, et un événement de streamingchild_run_linkedest émis pour corréler l’appel de l’outil parent avec leRunIDenfant.Streams et profils : Goa-AI publie des valeurs
stream.Eventtypées dans un flux détenu par la session (session/<session_id>). Les événements portentRunIDetSessionIDet le runtime émetrun_stream_endcomme marqueur explicite pour fermer SSE/WebSocket sans temporisateurs.stream.StreamProfilesélectionne les types d’événements visibles pour un public donné (UI chat, debug, métriques).
Démarrage rapide
package main
import (
"context"
chat "example.com/assistant/gen/orchestrator/agents/chat"
"goa.design/goa-ai/runtime/agent/model"
"goa.design/goa-ai/runtime/agent/runtime"
)
func main() {
// In-memory engine is the default; pass WithEngine for Temporal or custom engines.
rt := runtime.New()
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 := rt.CreateSession(ctx, "session-1"); 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.
}
Client-Only vs Worker
Deux rôles utilisent le runtime :
- Client-only (submit runs) : Construit un runtime avec un moteur compatible avec le client et n’enregistre pas d’agents. Il utilise le
<agent>.NewClient(rt)généré qui porte l’itinéraire (flux de travail + file d’attente) enregistré par les travailleurs distants. - Worker (exécuter des exécutions) : Construit un runtime avec un moteur capable de travailler, enregistre des agents (avec des planificateurs réels), et laisse le moteur interroger et exécuter des workflows/activités.
Exemple réservé au client
rt := runtime.New(runtime.WithEngine(temporalClient)) // engine client
// No agent registration needed in a caller-only process
client := chat.NewClient(rt)
if _, err := rt.CreateSession(ctx, "s1"); err != nil {
panic(err)
}
out, err := client.Run(ctx, "s1", msgs)
Exécutions one-shot sans session
Utilisez StartOneShot et OneShotRun lorsque vous voulez un travail durable qui n’est pas rattaché à une session existante.
Start/Runsont sessionnels : ils exigent unSessionIDconcret, participent au cycle de vie de la session et émettent des événements de flux portés par la session.StartOneShot/OneShotRunsont sans session : ils ne prennent pas deSessionID, n’en créent pas, et n’ajoutent que des événements canoniques au journal d’exécution pour l’introspection parRunID.StartOneShotrenvoie immédiatement unengine.WorkflowHandle.OneShotRunest le wrapper bloquant qui appellehandle.Wait(ctx)pour vous.
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)
Exemple de travailleur
rt := runtime.New(runtime.WithEngine(temporalWorker)) // worker-enabled engine
err := chat.RegisterChatAgent(ctx, rt, chat.ChatAgentConfig{Planner: myPlanner})
// Start engine worker loop per engine's integration (for example, Temporal worker.Run()).
Plan → Exécuter → Reprendre la boucle
- Le runtime démarre un workflow pour l’agent (en mémoire ou temporel) et enregistre un nouveau
run.ContextavecRunID,SessionID,TurnID, des labels et des caps de politique. - Il appelle le
PlanStartde votre planificateur avec les messages actuels et le contexte d’exécution. - Il planifie les appels d’outils renvoyés par le planificateur (le planificateur transmet des charges utiles JSON canoniques ; le runtime gère l’encodage/décodage à l’aide des codecs générés).
- Il appelle
PlanResumeavec les résultats de l’outil ; la boucle se répète jusqu’à ce que le planificateur renvoie une réponse finale ou que les plafonds/les budgets de temps soient atteints. Au fur et à mesure que l’exécution progresse, elle passe par les valeursrun.Phase(prompted,planning,executing_tools,synthesizing, phases terminales). - Les crochets et les abonnés au flux émettent des événements (pensées du planificateur, démarrage/mise à jour/fin de l’outil, attentes, utilisation, flux de travail, liens agent-exécution) et, lorsqu’ils sont configurés, conservent les entrées de transcription et les métadonnées d’exécution.
Phases d’exécution
Au fur et à mesure qu’une exécution progresse dans la boucle planifier/exécuter/reprendre, elle passe par une série de phases du cycle de vie. Ces phases offrent une visibilité fine de l’état d’avancement de l’exécution, ce qui permet aux interfaces utilisateur d’afficher des indicateurs de progression de haut niveau.
Valeurs des phases
| Phase | Description |
|---|---|
| Le planificateur est en train de décider comment appeler les outils ou répondre directement | |
planning Le planificateur est en train de décider s’il doit appeler des outils ou répondre directement, et comment | |
executing_tools Les outils (y compris les agents imbriqués) sont en cours d’exécution | |
synthesizing | Le planificateur est en train de synthétiser une réponse finale sans programmer d’outils supplémentaires |
completed | L’exécution s’est terminée avec succès |
failed | L’exécution a échoué |
canceled | L’exécution a été annulée |
Transitions de phase
Une exécution réussie typique suit la progression suivante :
prompted → planning → executing_tools → planning → synthesizing → completed
↑__________________|
(loop while tools needed)
Le moteur d’exécution émet des événements RunPhaseChanged pour les phases non terminales (par exemple planning, executing_tools, synthesizing) afin que les abonnés au flux puissent suivre la progression en temps réel.
Phase vs Statut
Les phases sont distinctes des run.Status :
- Status (
pending,running,completed,failed,canceled,paused) est l’état du cycle de vie à gros grain stocké dans les métadonnées d’exécution durables - La phase offre une visibilité plus fine de la boucle d’exécution, destinée aux surfaces de streaming/UX
Événements de cycle de vie : changements de phase vs fin d’exécution
Le runtime émet :
RunPhaseChangedpour les transitions de phase non terminales.RunCompletedune seule fois par run pour l’état terminal (success / failed / canceled).
Les abonnés au flux traduisent les deux en événements workflow (stream.WorkflowPayload) :
- Mises à jour non terminales (depuis
RunPhaseChanged) :phaseuniquement. - Mise à jour terminale (depuis
RunCompleted) :status+phaseterminale, plus des champs d’erreur structurés en cas d’échec.
Mapping du status terminal
status="success"→phase="completed"status="failed"→phase="failed"status="canceled"→phase="canceled"
L’annulation n’est pas une erreur
Pour status="canceled", le payload de stream ne doit pas inclure de message error destiné à l’utilisateur. Les consommateurs doivent traiter l’annulation comme un état terminal non erroné.
Les échecs sont structurés
Pour status="failed", le payload de stream inclut :
error_kind: classificateur stable pour l’UX/décision (kinds provider commerate_limited,unavailable, ou kinds runtime commetimeout/internal)retryable: indique si une nouvelle tentative peut réussir sans modifier l’entréeerror: message sûr pour l’utilisateur (affichage direct)debug_error: erreur brute pour logs/diagnostics (pas pour UI)
Politiques, plafonds et étiquettes
Politique d’exécution au moment de la conception
Au moment de la conception, vous configurez les politiques par agent avec RunPolicy :
Agent("chat", "Conversational runner", func() {
RunPolicy(func() {
DefaultCaps(
MaxToolCalls(8),
MaxConsecutiveFailedToolCalls(3),
)
TimeBudget("2m")
InterruptsAllowed(true)
})
})
Cela devient un runtime.RunPolicy attaché à l’enregistrement de l’agent :
- Caps :
MaxToolCalls- nombre total d’appels à l’outil par exécution.MaxConsecutiveFailedToolCalls- échecs consécutifs avant l’abandon. - Budget temps :
TimeBudget- budget temps pour l’exécution.FinalizerGrace(temps d’exécution uniquement) - fenêtre réservée facultative pour la finalisation. - Interruptions :
InterruptsAllowed- option de pause/reprise. - Comportement en cas de champs manquants :
OnMissingFields- régit ce qui se passe lorsque la validation indique des champs manquants.
Remplacement des politiques d’exécution
Dans certains environnements, vous pouvez souhaiter renforcer ou assouplir les règles sans modifier la conception. L’API rt.OverridePolicy permet d’ajuster les politiques au niveau du processus :
err := rt.OverridePolicy(chat.AgentID, runtime.RunPolicy{
MaxToolCalls: 3,
MaxConsecutiveFailedToolCalls: 1,
InterruptsAllowed: true,
})
Scope : Les dérogations sont locales à l’instance d’exécution actuelle et n’affectent que les exécutions suivantes. Elles ne persistent pas lors des redémarrages de processus et ne se propagent pas aux autres travailleurs.
Champs de surcharge :
| Champ | Description |
|---|---|
MaxToolCalls | Total maximum d’appels d’outils par exécution |
MaxConsecutiveFailedToolCalls | Échecs consécutifs avant l’abandon |
TimeBudget | Budget de l’horloge murale pour l’exécution |
FinalizerGrace FinalizerGrace FinalizerGrace Fenêtre réservée pour la finalisation | |
InterruptsAllowed | Activation de la capacité de pause/reprise |
Seuls les champs non nuls sont appliqués (et InterruptsAllowed lorsque true). Cela permet d’effectuer des remplacements sélectifs sans affecter les autres paramètres de la politique.
Cas d’utilisation :
- Remboursements temporaires lors de l’étranglement du fournisseur d’accès
- Tests A/B de différentes configurations de politiques
- Développement/débogage avec des contraintes relâchées
- Personnalisation de la politique par locataire au moment de l’exécution
Étiquettes et moteurs de politique
Goa-AI s’intègre à des moteurs de politiques enfichables via policy.Engine. Les politiques reçoivent les métadonnées des outils (ID, tags), le contexte d’exécution (SessionID, TurnID, étiquettes) et les informations RetryHint après les échecs des outils.
Les étiquettes sont transférées dans :
run.Context.Labels- disponibles pour les planificateurs au cours d’une exécution- entrée d’activité d’outil (
api.ToolInput.Labels) - clonée dans les exécutions d’outils dispatchées afin que les activités observent les mêmes métadonnées d’exécution, sauf si le planificateur remplace les labels pour un appel précis - journal d’exécution (
runlog.Store) - persisté avec les événements de cycle de vie pour audit/recherche/tableaux de bord (lorsqu’indexé)
Exécution de l’outil
- Outils natifs : Vous écrivez les implémentations ; le runtime gère le décodage des args typés en utilisant les codecs générés
- Outil généré en tant qu’outil : Les outils d’agent générés exécutent les agents fournisseurs en tant qu’exécutions enfant (en ligne du point de vue du planificateur) et adaptent leur
RunOutputen unplanner.ToolResultavec une poignéeRunLinkrenvoyée à l’exécution enfant - OutilsMCP : Le moteur d’exécution transmet le JSON canonique aux appelants générés ; les appelants gèrent le transport
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.
Contrats runtime des prompts
La gestion des prompts est native au runtime et versionnee :
runtime.PromptRegistrystocke des enregistrements immuables de prompt specs de base (prompt.PromptSpec).runtime.WithPromptStore(prompt.Store)active la resolution d’overrides scopes (session->facility->org-> global).- Les planners appellent
PlannerContext.RenderPrompt(ctx, id, data)pour resoudre et rendre le contenu. - Le contenu rendu inclut des metadonnees
prompt.PromptRefpour la provenance ; les planners peuvent les attacher amodel.Request.PromptRefs.
content, err := input.Agent.RenderPrompt(ctx, "aura.chat.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: input.Messages,
PromptRefs: []prompt.PromptRef{content.Ref},
})
PromptRefs sont des metadonnees runtime d’audit/provenance et ne font pas partie des champs wire payload du fournisseur.
Mémoire, flux, télémétrie
Le bus de crochet publie des événements de crochet structurés pour le cycle de vie complet de l’agent : démarrage/achèvement de l’exécution, changements de phase,
prompt_rendered, programmation/résultats/mises à jour de l’outil, notes du planificateur et blocs de réflexion, attentes, indices de réessai et liens entre l’agent et l’outil.Les magasins de mémoire (
memory.Store) souscrivent et ajoutent des événements de mémoire durables (messages de l’utilisateur/assistant, appels d’outils, résultats d’outils, notes du planificateur, réflexion) par(agentID, RunID).Magasins d’événements d’exécution (
runlog.Store) ajoutent le journal canonique des événements hook parRunIDpour UI audit/debug et introspection.Les puits de flux (
stream.Sink, par exemple Pulse ou SSE/WebSocket personnalisé) reçoivent les valeurs typéesstream.Eventproduites par lestream.Subscriber. UneStreamProfilecontrôle les types d’événements émis.Télémétrie : La journalisation, la métrologie et le traçage des flux de travail et des activités de bout en bout sont pris en compte par OTEL.
Indices d’appel d’outil (DisplayHint)
Les appels d’outils peuvent transporter un DisplayHint destiné à l’utilisateur (par exemple, pour des UI).
Contrat :
- Les constructeurs d’événements de hooks ne rendent pas les indices. Les événements de planification d’outil ont
DisplayHint==""par défaut. - Le runtime peut enrichir et persister un indice d’appel durable au moment de la publication en décodant la charge utile typée et en exécutant le
CallHintTemplateDSL. - Si le décodage typé échoue ou si aucun modèle n’est enregistré, le runtime laisse
DisplayHintvide. Les indices ne sont jamais rendus à partir de JSON brut. - Si un producteur définit explicitement
DisplayHint(non vide) avant de publier l’événement hook, le runtime le considère comme faisant autorité et ne l’écrase pas. - Pour des variations par consommateur (par exemple, une formulation UI différente), configurez
runtime.WithHintOverridessur le runtime. Les overrides ont la priorité sur les templates DSL pour les événementstool_startstreamés.
Consommer le flux de session (Pulse)
En production, le schéma habituel est :
- consommer le flux de session (
session/<session_id>) depuis un bus partagé (Pulse / Redis Streams) - filtrer par
run_idpour construire des cartes/voies par exécution - fermer SSE/WebSocket à l’observation de
run_stream_endpour lerun_idactif
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)
}
}
Abstraction du moteur
- En mémoire : Boucle de développement rapide, pas de développement externe
- Temporel : Exécution durable, relecture, tentatives, signaux, travailleurs ; les adaptateurs câblent les activités et la propagation du contexte
Temporisation sémantique vs vivacité Temporal
Goa-AI garde le contrat runtime public agnostique du moteur :
RunPolicy.Timing.PlanetRunPolicy.Timing.Toolssont des budgets sémantiques par tentativeruntime.WithTiming(...)surcharge ces budgets sémantiques pour une exécutionruntime.WithWorker(...)sert au placement sur file, pas au réglage du moteur de workflow
Si vous utilisez l’adaptateur Temporal et que vous devez régler l’attente en file ou la vivacité, configurez ces mécanismes sur le moteur Temporal lui-même :
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)
}
Cette séparation garde la mécanique des workflows derrière la frontière Temporal, tandis que le runtime générique reste fidèle à la fois à Temporal et au moteur en mémoire.
Exécuter les contrats
SessionIDest requis pour les démarrages sessionnels.StartetRunéchouent rapidement siSessionIDest vide ou contient des espacesStartOneShotetOneShotRunsont explicitement sans session. Ils n’exigent ni ne créent de session et n’émettent pas d’événements de flux portés par la session- Les agents doivent être enregistrés avant la première exécution. Le moteur d’exécution rejette l’enregistrement après la soumission de la première exécution avec
ErrRegistrationClosedafin de maintenir le caractère déterministe des travailleurs du moteur - Les exécuteurs d’outils reçoivent des métadonnées explicites par appel (
ToolCallMeta) plutôt que des valeurs de pêche provenant decontext.Context - Ne pas s’appuyer sur des fallbacks implicites ; tous les identifiants de domaine (run, session, turn, correlation) doivent être transmis explicitement
Pause et reprise
Les flux de travail humains en boucle peuvent suspendre et reprendre leur exécution à l’aide des aides à l’interruption du runtime :
import "goa.design/goa-ai/runtime/agent/interrupt"
// Pause
if err := rt.PauseRun(ctx, interrupt.PauseRequest{
RunID: "session-1-run-1",
Reason: "human_review",
}); err != nil {
panic(err)
}
// Resume
if err := rt.ResumeRun(ctx, interrupt.ResumeRequest{
RunID: "session-1-run-1",
}); err != nil {
panic(err)
}
En coulisses, les signaux de pause/reprise mettent à jour le magasin d’exécution et émettent des événements de type run_paused/run_resumed pour que les couches de l’interface utilisateur restent synchronisées.
Fournir des résultats d’outils externes
Certaines attentes se reprennent avec des résultats d’outils fournis par un acteur externe plutôt que par ExecuteToolActivity lui-même. Les cas typiques sont les outils pilotés par l’interface utilisateur, comme les questions structurées, ou les services passerelles qui collectent des résultats depuis un autre système avant de réveiller l’exécution.
Utilisez ProvideToolResults avec des résultats bruts fournis :
err := rt.ProvideToolResults(ctx, interrupt.ToolResultsSet{
RunID: "run-123",
ID: "await-1",
Results: []*api.ProvidedToolResult{
{
Name: "chat.ask_question.ask_question",
ToolCallID: "toolcall-1",
Result: rawjson.Message(`{"answers":[{"question_id":"topic","selected_ids":["alarms"]}]}`),
},
},
})
Contrat :
- Les appelants fournissent le JSON de résultat canonique brut ainsi que
Bounds,ErroretRetryHinten option. - Les appelants ne construisent pas
api.ToolEvent; c’est l’enveloppe interne de workflow du runtime. - Le runtime décode le résultat fourni à l’aide de la spécification d’outil enregistrée, exécute la matérialisation typée du résultat, attache les sidecars purement serveur, ajoute le
tool_resultcanonique au transcript/run log, puis reprend seulement ensuite la planification.
Cela maintient le chemin d’attente conceptuellement aligné sur le chemin d’exécution normal : les deux flux convergent vers le même contrat typé planner.ToolResult avant publication.
Confirmation de l’outil
Goa-AI supporte des portes de confirmation forcées par le temps d’exécution pour les outils sensibles (écritures, suppressions, commandes).
Vous pouvez activer la confirmation de deux manières :
- Temps de conception (cas courant): déclarer
Confirmation(...)à l’intérieur du DSL de l’outil. Codegen stocke la politique danstools.ToolSpec.Confirmation. - Runtime (override/dynamique): passer
runtime.WithToolConfirmation(...)lors de la construction du runtime pour exiger la confirmation d’outils supplémentaires ou remplacer le comportement du temps de conception.
Au moment de l’exécution, le flux de travail émet une demande de confirmation hors bande et n’exécute l’outil qu’après avoir reçu une approbation explicite qu’après avoir reçu une approbation explicite. En cas de refus, la durée d’exécution synthétise un résultat d’outil conforme au schéma, de manière à ce que la transcription reste valide et que l’outil soit exécuté afin que la transcription reste valide et que le planificateur puisse réagir de manière déterministe.
Protocole de confirmation
Lors de l’exécution, la confirmation est mise en œuvre sous la forme d’un protocole d’attente/décision dédié :
Await payload (streamed as
await_confirmation) :{ "id": "...", "title": "...", "prompt": "...", "tool_name": "atlas.commands.change_setpoint", "tool_call_id": "toolcall-1", "payload": { "...": "canonical tool arguments (JSON)" } }
Contrat :
payloadcontient toujours les arguments JSON canoniques de l’appel d’outil en attente. Si l’appel est approuvé, ce sont ces arguments que le runtime exécute.Les surcharges de confirmation peuvent personnaliser le prompt et le rendu du résultat refusé, mais elles n’introduisent pas de canal de payload d’affichage séparé et ne changent pas la signification de
payload.Les produits qui ont besoin d’une UI de confirmation plus riche doivent la matérialiser dans la couche application à partir du payload canonique et de lectures détenues par l’application.
Fournir une décision (via
ProvideConfirmationsur le runtime) :err := rt.ProvideConfirmation(ctx, interrupt.ConfirmationDecision{ RunID: "run-123", 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"}, })
Événements d’autorisation d’outil
Lorsqu’une décision est fournie, le runtime émet un événement d’autorisation de premier ordre :
- Hook event :
hooks.ToolAuthorization - Stream event type :
tool_authorization
Cet événement est l’enregistrement canonique “qui/quand/quoi” pour un appel d’outil confirmé :
tool_name,tool_call_idapproved(true/false)summary(résumé déterministe rendu par le runtime)approved_by(copié depuisinterrupt.ConfirmationDecision.RequestedBy, identifiant de principal stable)
L’événement est émis immédiatement après réception de la décision (avant l’exécution de l’outil si approuvé, et avant la synthèse du résultat refusé si refusé).
Notes :
- Les consommateurs doivent traiter la confirmation comme un protocole d’exécution :
- Utilisez le motif
RunPaused(await_confirmation) qui l’accompagne pour décider quand afficher une interface utilisateur de confirmation. - N’associez pas le comportement de l’interface utilisateur à un nom d’outil de confirmation spécifique ; traitez-le comme un détail de transport interne.
- Utilisez le motif
- Les modèles de confirmation (
PromptTemplateetDeniedResultTemplate) sont des chaînes Gotext/templateexécutées avecmissingkey=error. Outre les fonctions standard des modèles (par exemple,printf), Goa-AI fournit :json v→ JSON encodev(utile pour les champs de pointeurs optionnels ou l’intégration de valeurs structurées).quote s→ renvoie une chaîne entre guillemets Go-escapée (commefmt.Sprintf("%q", s)).
Validation au moment de l’exécution
Le moteur d’exécution valide les interactions de confirmation à la frontière :
- La confirmation
IDcorrespond à l’identifiant de l’attente en cours lorsqu’il est fourni. - L’objet de décision est bien formé (
RunIDnon vide, valeur booléenneApproved).
Contrat de planification
Les planificateurs mettent en œuvre :
type Planner interface {
PlanStart(ctx context.Context, input *planner.PlanInput) (*planner.PlanResult, error)
PlanResume(ctx context.Context, input *planner.PlanResumeInput) (*planner.PlanResult, error)
}
PlanResult contient les appels d’outils, la réponse finale, les annotations et RetryHint facultatif. Le moteur d’exécution applique les caps, planifie les activités des outils et renvoie les résultats des outils dans PlanResume jusqu’à ce qu’une réponse finale soit produite.
Les planificateurs reçoivent également un PlannerContext via input.Agent qui expose les services d’exécution :
AdvertisedToolDefinitions()- obtenir les définitions d’outils filtrées par le runtime et visibles par le modèle pour ce tourModelClient(id string)- obtenir un client de modèle brut agnostique par rapport au fournisseurPlannerModelClient(id string)- obtenir un client de modèle propre au planner avec émission d’événements gérée par le runtimeRenderPrompt(ctx, id, data)- resoudre et rendre le contenu de prompt pour le scope de run courantAddReminder(r reminder.Reminder)- enregistrer des rappels de système à l’échelle de l’exécutionRemoveReminder(id string)- effacer les rappels lorsque les conditions préalables ne sont plus rempliesMemory()- accès à l’historique des conversations
Modules de fonctionnalités
features/mcp/*- suite MCP DSL/codegen/appels en temps réel (HTTP/SSE/stdio)features/memory/mongo- mémoire durablefeatures/prompt/mongo- prompt store Mongo pour les overrides de promptsfeatures/runlog/mongo- magasin d’événements d’exécution (append-only, pagination par curseur)features/session/mongo- magasin de métadonnées de sessionfeatures/stream/pulse- aides pour les puits d’impulsion et les abonnésfeatures/model/{anthropic,bedrock,openai}- adaptateurs de client de modèle pour les planificateursfeatures/model/middleware- middlewares partagésmodel.Client(par exemple, limitation adaptative du débit)features/policy/basic- moteur de politique simple avec listes d’autorisation/de blocage et gestion des indices de réessai
Modélisation du débit du client et de la limitation du débit
Goa-AI fournit un limiteur de débit adaptatif agnostique au fournisseur sous features/model/middleware. Il enveloppe tout model.Client, estime les jetons par demande, met les appelants en file d’attente en utilisant un seau de jetons, et ajuste son budget effectif de jetons par minute en utilisant une stratégie additive-incrémentale/multiplicative-décrémentale (AIMD) lorsque les fournisseurs signalent un étranglement.
import (
"goa.design/goa-ai/features/model/bedrock"
mdlmw "goa.design/goa-ai/features/model/middleware"
)
awsClient := bedrockruntime.NewFromConfig(cfg)
bed, _ := bedrock.New(awsClient, bedrock.Options{
DefaultModel: "us.anthropic.claude-4-5-sonnet-20251120-v1:0",
}, ledger)
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 := rl.Middleware()(bed)
rt := runtime.New(runtime.Options{
// Register limited as the model client exposed to planners.
})
Intégration LLM
Les planificateurs Goa-AI interagissent avec les grands modèles de langage par le biais d’une interface agnostique au fournisseur. Cette conception vous permet de changer de fournisseur - AWS Bedrock, OpenAI, ou des points d’extrémité personnalisés - sans modifier le code de votre planificateur.
L’interface model.Client
Toutes les interactions LLM passent par l’interface model.Client :
type Client interface {
Complete(ctx context.Context, req *Request) (*Response, error)
Stream(ctx context.Context, req *Request) (Streamer, error)
}
Adaptateurs de fournisseurs
Goa-AI est livré avec des adaptateurs pour les fournisseurs LLM les plus courants :
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,
}, ledger)
OpenAI
import "goa.design/goa-ai/features/model/openai"
modelClient, err := openai.NewFromAPIKey(apiKey, "gpt-4o")
Utilisation de clients modèles dans les planificateurs
Les planificateurs obtiennent des clients de modèle via le PlannerContext du
runtime. Il existe désormais deux styles d’intégration explicites :
PlannerModelClient(id)pour un streaming propre au planner avec émission d’événements gérée par le runtimeModelClient(id)lorsque vous avez besoin d’un accès brut au transport et que vous l’associerez àplanner.ConsumeStreamou que vous émettrezPlannerEventsvous-même
PlannerModelClient (Recommandé)
PlannerContext.PlannerModelClient(id) renvoie un client propre au planner qui
prend en charge l’émission de AssistantChunk, PlannerThinkingBlock et
UsageDelta. Sa méthode Stream(...) draine le flux du fournisseur sous-jacent
et renvoie un planner.StreamSummary :
func (p *MyPlanner) PlanStart(ctx context.Context, input *planner.PlanInput) (*planner.PlanResult, error) {
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: input.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
}
return &planner.PlanResult{
FinalResponse: &planner.FinalResponse{
Message: &model.Message{
Role: model.ConversationRoleAssistant,
Parts: []model.Part{model.TextPart{Text: sum.Text}},
},
},
Streamed: true, // Assistant text was already streamed
}, nil
}
Il s’agit du style d’intégration le plus sûr, car le client propre au planner
n’expose pas de model.Streamer brut et ne peut donc pas être combiné par
accident avec planner.ConsumeStream.
Client brut + ConsumeStream
Lorsque vous avez besoin du model.Client brut, récupérez-le via
PlannerContext.ModelClient et associez-le à planner.ConsumeStream :
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: input.Messages,
Tools: input.Agent.AdvertisedToolDefinitions(),
Stream: true,
}
streamer, err := mc.Stream(ctx, req)
if err != nil {
return nil, err
}
sum, err := planner.ConsumeStream(ctx, streamer, req, input.Events)
if err != nil {
return nil, err
}
Ce helper draine le flux, émet les événements d’assistant/de réflexion/d’usage,
et renvoie un StreamSummary avec le texte accumulé et les appels d’outils.
Utilisez la voie du client brut lorsque vous avez besoin d’un contrôle total sur
la consommation du flux, d’un comportement d’arrêt anticipé personnalisé, ou
que vous souhaitez gérer explicitement PlannerEvents. Ne mélangez pas
PlannerModelClient.Stream(...) avec planner.ConsumeStream ; choisissez un
seul propriétaire du flux par tour de planner.
Validation de l’ordre des messages de Bedrock
Lors de l’utilisation de AWS Bedrock avec le mode de pensée activé, le runtime valide les contraintes d’ordre des messages avant d’envoyer des requêtes. Bedrock requiert :
- Tout message d’assistant contenant
tool_usedoit commencer par un bloc de réflexion - Chaque message d’utilisateur contenant
tool_resultdoit suivre immédiatement un message d’assistant avec les blocstool_usecorrespondants - Le nombre de blocs
tool_resultne peut dépasser le nombre de blocstool_useprécédents
Le client Bedrock valide ces contraintes en amont et renvoie une erreur descriptive en cas de violation :
bedrock: invalid message ordering with thinking enabled (run=xxx, model=yyy):
bedrock: assistant message with tool_use must start with thinking
Cette validation garantit que la reconstruction du grand livre de transcription produit des séquences de messages conformes au fournisseur.
Prochaines étapes
- En savoir plus sur Toolsets pour comprendre les modèles d’exécution d’outils
- Explorer Composition des agents pour les modèles d’agents en tant qu’outils
- En savoir plus sur Memory & Sessions pour la persistance des transcriptions