Démarrage rapide
Ce guide vous fait passer d’un module vide à un agent Goa-AI généré et exécutable. L’exemple généré utilise le moteur en mémoire, vous n’avez donc pas besoin de Temporal, MongoDB, Redis ou une clé modèle API pour la première exécution.
Vous construirez :
- Une conception Goa avec un agent, un outil typé et une complétion directe typée.
- Agent généré, ensemble d’outils, code de complétion et de câblage d’exécution.
- Un exemple d’échafaudage exécutable avec un planificateur de stub que vous pouvez remplacer par un planificateur basé sur un modèle.
- Les premiers hooks de production : sessions explicites, exécuteurs d’outils générés, streaming et enregistrement de modèle.
1. Créez un module
GOPROXY=direct go install goa.design/goa/v3/cmd/goa@fix/goa-generation-plan
mkdir quickstart && cd quickstart
go mod init example.com/quickstart
GOPROXY=direct go get goa.design/goa/v3@fix/goa-generation-plan goa.design/goa-ai@main
mkdir design
Ces noms de branche sélectionnent le contrat de stockage intégré du runtime
utilisé dans ce guide. Le réglage du proxy direct est nécessaire, car le nom de
la branche préliminaire de Goa contient une barre oblique. Go enregistre des
pseudo-versions exactes dans go.mod ; les mises à jour ultérieures des branches
ne modifient donc pas une compilation existante tant que vous ne relancez pas
go get.
Goa-AI cible actuellement le Go moderne. Utilisez la version Go déclarée par le
Module goa.design/goa-ai ou plus récent.
2. Définir l’agent
Créez design/design.go :
package design
import (
. "goa.design/goa/v3/dsl"
. "goa.design/goa-ai/dsl"
)
var _ = API("orchestrator", func() {})
var AskPayload = Type("AskPayload", func() {
Attribute("question", String, "User question to answer")
Example(map[string]any{"question": "What is the capital of Japan?"})
Required("question")
})
var Answer = Type("Answer", func() {
Attribute("text", String, "Answer text")
Example(map[string]any{"text": "Tokyo is the capital of Japan."})
Required("text")
})
var TaskDraft = Type("TaskDraft", func() {
Attribute("name", String, "Task name")
Attribute("goal", String, "Outcome-style goal")
Example(map[string]any{"name": "Prepare launch checklist", "goal": "Confirm the service is ready to launch."})
Required("name", "goal")
})
var _ = Service("orchestrator", func() {
Completion("draft_task", "Produce a task draft directly", func() {
Return(TaskDraft)
})
Agent("chat", "Friendly Q&A assistant", func() {
Use("helpers", func() {
Tool("answer", "Answer a simple question", func() {
Args(AskPayload)
Return(Answer)
})
})
RunPolicy(func() {
DefaultCaps(MaxToolCalls(2), MaxRecoveryTurns(1))
TimeBudget("15s")
})
})
})
C’est la source de la vérité. Les outils et les complétions réutilisent les types Goa normaux, descriptions, exemples et validations. Les schémas orientés modèle, les codecs typés, et les contrats d’exécution sont générés à partir de cette conception.
3. Générer du code et un exemple
goa gen example.com/quickstart/design
goa example example.com/quickstart/design
go mod tidy
go run ./cmd/orchestrator
Forme attendue :
RunID: demo-chat-run
Assistant: Tool helpers.answer returned {"text":"Tokyo is the capital of Japan."}
Completion draft_task: &{Name:Prepare launch checklist Goal:Confirm the service is ready to launch.}
Completion delta draft_task: {"goal":"Confirm the ser
Completion stream draft_task: &{Name:Prepare launch checklist Goal:Confirm the service is ready to launch.}
La ligne Completion delta est un préfixe JSON transmis en streaming. Son point
de coupure exact peut varier, mais la valeur finale transmise est complète et
correspond à l’exemple déclaré.
goa gen crée des contrats générés. goa example crée des applications appartenant
échafaudage :
gen/: code généré. Ne modifiez pas ce répertoire à la main.cmd/orchestrator/main.go: exemple de point d’entrée exécutable.internal/agents/orchestrator/bootstrap/bootstrap.go: construction du runtime et enregistrement des agents.internal/agents/chat/planner/planner.go: planificateur de stub à remplacer.gen/orchestrator/completions/: assistants de saisie semi-automatique typés.
Régénérer après les modifications de DSL. Réexécutez goa example lorsque vous souhaitez un échafaudage
mises à jour, puis conservez les modifications de l’application dans cmd/ et internal/.
4. Comprendre la boucle d’exécution
La boucle planifier/exécuter :
PlanStartreçoit les messages utilisateur initiaux.- Le planificateur renvoie un
FinalResponse, des appels d’outils ou une demande d’attente. - Le runtime valide et exécute les appels d’outils admis à l’aide des spécifications générées et des exécuteurs enregistrés.
PlanResumereçoit les sorties d’outils visibles par le planificateur.- La boucle se répète jusqu’à ce que le planificateur renvoie une réponse finale, un résultat d’outil de terminal ou que le moteur d’exécution applique des plafonds/budgets de temps.
Lorsque des plafonds ou deadlines forcent la finalisation, un planificateur peut
encore fermer l’exécution en renvoyant des outils terminaux de comptabilité. Le
runtime n’exécute que des outils TerminalRun() dans ce chemin
(TerminalRun() implique la comptabilité) et exige leur succès avant de
considérer l’exécution comme fermée.
L’exemple généré commence avec un planificateur factice afin de montrer ce flux
avant la connexion d’un modèle. Lorsqu’un outil possède un exemple de charge
utile, PlanStart construit l’appel avec
planner.NewToolRequest(gentool.<Tool>Tool(), args). PlanResume examine
ToolOutput.Failure et transforme le JSON canonique d’un résultat réussi en
message final de l’assistant. Un outil sans résultat réussit avec un résultat
vide. Un vrai planificateur suit le même contrat et délègue la décision
sémantique à un client de modèle.
5. Appelez l’agent depuis le code
Les packages d’agent générés exposent les clients typés. Les exécutions de session nécessitent un séance explicite ; Les exécutions ponctuelles sont intentionnellement sans session.
store := storageinmem.New()
if _, err := store.CreateSession(ctx, "session-1", time.Now().UTC()); err != nil {
log.Fatal(err)
}
rt, cleanup, err := bootstrap.New(ctx, store)
if err != nil {
log.Fatal(err)
}
defer cleanup()
client := chat.NewClient(rt)
out, err := client.Run(ctx, "session-1", []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Hello"}},
}})
if err != nil {
log.Fatal(err)
}
fmt.Println(out.RunID)
out, err = client.OneShotRun(ctx, []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Summarize this document"}},
}})
Utilisez Run ou Start pour le travail conversationnel/sessionnel. Utilisez OneShotRun ou
StartOneShot pour les tâches de demande/réponse qui doivent être observables par RunID
mais ne doit pas appartenir à une session.
Le projet local généré accepte un storage.Store et la commande d’exemple utilise runtime/agent/storage/inmem. En production, l’application fournit un adaptateur vers le service propriétaire de la base de données du runtime. Ce service, et non un worker d’agent, crée et termine les sessions.
6. Implémenter un exécuteur d’outils
Les packages d’agents générés incluent une fonction RegisterUsedToolsets pour
les ensembles d’outils locaux. Les exécuteurs reçoivent des métadonnées
d’exécution explicites et renvoient un résultat possédé par le runtime. Chaque
package de spécifications expose aussi un descripteur typé par outil — ici
helpers.AnswerTool — qui associe l’identifiant aux codecs générés de charge
utile et de résultat. Le décodage est ainsi vérifié à la compilation, sans
assertion de type ni association nom-codec répétée à la main :
type HelpersExecutor struct{}
func (e *HelpersExecutor) Execute(
ctx context.Context,
meta *runtime.ToolCallMeta,
call *runtime.ToolCall,
) (*runtime.ToolExecutionResult, error) {
switch call.Name {
case helpers.Answer:
args, err := helpers.AnswerTool().Payload.FromJSON(call.Payload)
if err != nil {
return nil, fmt.Errorf("decode admitted %s payload: %w", call.Name, err)
}
return runtime.Executed(&planner.ToolResult{
Name: call.Name,
Result: &helpers.AnswerResult{Text: "Answer: " + args.Question},
}), nil
default:
return runtime.Executed(&planner.ToolResult{
Name: call.Name,
Failure: &planner.ToolFailure{
Kind: planner.FailureInvalidCall,
Error: planner.NewToolError("unknown tool"),
Recovery: planner.RecoveryDirective{Action: planner.RecoveryReplan},
},
}), nil
}
}
if err := chat.RegisterUsedToolsets(ctx, rt, chat.WithHelpersExecutor(&HelpersExecutor{})); err != nil {
log.Fatal(err)
}
Le runtime valide la charge utile JSON avec les codecs générés avant l’exécution,
encode les résultats réussis avec les codecs de résultat générés, enregistre l’exécution canonique
événements et transmet les sorties visibles par le planificateur à PlanResume.
7. Connectez un modèle
Enregistrez les clients du fournisseur auprès du runtime, puis accédez-y à partir des planificateurs par ID.
Pour les planificateurs de streaming, préférez PlannerModelClient ; il possède un assistant/réfléchissant
et l’émission d’événements d’utilisation. Cet exemple utilise OpenAI ; voir
Runtime → Intégration LLM pour les équivalents
AWS Bedrock et Google Vertex AI (Gemini et Claude-on-Vertex).
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 {
log.Fatal(err)
}
if err := rt.RegisterModel("default", modelClient); err != nil {
log.Fatal(err)
}
Croquis du planificateur :
func (p *Planner) PlanStart(ctx context.Context, in *planner.PlanInput) (*planner.PlanResult, error) {
mc, ok := in.Agent.PlannerModelClient("default")
if !ok {
return nil, errors.New("model client default is not registered")
}
summary, err := mc.Stream(ctx, &model.Request{
Messages: in.Messages,
Tools: in.Agent.AdvertisedToolDefinitions(),
Stream: true,
})
if err != nil {
return nil, err
}
if len(summary.ToolCalls) > 0 {
return &planner.PlanResult{ToolCalls: summary.ToolCalls}, nil
}
final := summary.FinalResponse()
if final == nil {
return nil, errors.New("model stream ended without a canonical response")
}
return &planner.PlanResult{
FinalResponse: final,
Streamed: true,
}, nil
}
Utilisez in.Agent.ModelClient("default") lorsque vous devez lire vous-même le
flux validé avec planner.ConsumeStream(ctx, stream). Choisissez un seul
propriétaire du flux par tour du planificateur. Les constructeurs de
fournisseurs renvoient des clients opaques dont les réponses sont validées
avant d’atteindre le planificateur.
8. Ajouter du streaming
Goa-AI émet des événements de flux typés pour le texte de l’assistant, les démarrages/fins d’outils, le flux de travail
statut, attentes, utilisation et liens d’exécution enfants. Câblez n’importe quel stream.Sink :
type ConsoleSink struct{}
func (s *ConsoleSink) Send(ctx context.Context, event stream.Event) error {
switch e := event.(type) {
case stream.AssistantReply:
fmt.Print(e.Data.Text)
case stream.ToolStart:
fmt.Printf("tool_start: %s\n", e.Data.ToolName)
case stream.ToolEnd:
fmt.Printf("tool_end: %s\n", e.Data.ToolName)
case stream.Workflow:
fmt.Printf("workflow: %s\n", e.Data.Phase)
}
return nil
}
func (s *ConsoleSink) Close(ctx context.Context) error { return nil }
rt := runtime.New(runtimeStore, runtime.WithStream(&ConsoleSink{}))
Pour la production UIs, publiez sur Pulse et abonnez-vous au flux de session
(session/<session_id>). Fermez la connexion utilisateur lorsque vous observez
run_stream_end pour l’exécution active.
9. Utiliser les complétions directes typées
Completion(...) est destiné à la sortie structurée de l’assistant qui n’est pas un appel d’outil.
Les assistants générés demandent une sortie structurée imposée par le fournisseur et décodent via
codecs générés :
resp, err := completions.CompleteDraftTask(ctx, modelClient, &model.Request{
Messages: []*model.Message{{
Role: model.ConversationRoleUser,
Parts: []model.Part{model.TextPart{Text: "Draft a task for launch readiness."}},
}},
})
if err != nil {
log.Fatal(err)
}
fmt.Println(resp.Value.Name)
Les noms de complétion font partie du contrat de sortie structurée : 1-64 ASCII
caractères, lettres/chiffres/_/-, commençant par une lettre ou un chiffre. Diffusion en continu
les aides à l’achèvement exposent les morceaux d’aperçu completion_delta et décodent uniquement les
morceau canonique final completion.
10. Composer des agents
Les agents peuvent exporter des ensembles d’outils que d’autres agents utilisent. Les agents imbriqués sont exécutés en tant qu’enfants
workflows avec leur propre RunID, et les flux émettent child_run_linked afin que UIs puisse
rendre les arbres d’exécution.
Agent("researcher", "Research specialist", func() {
Export("research", func() {
Tool("deep_search", "Perform deep research", func() {
Args(ResearchRequest)
Return(ResearchReport)
})
})
})
Agent("coordinator", "Delegates specialist work", func() {
Use(AgentToolset("orchestrator", "researcher", "research"))
})
Chaque agent conserve son propre planificateur, ses outils et sa politique. Le stockage du runtime fourni par l’application enregistre chaque exécution racine et enfant, y compris le lien vers le parent. Le parent reçoit un résultat d’outil normal avec un RunLink vers l’exécution enfant.
Ce que vous avez construit
- Un agent axé sur la conception avec des outils validés par les schémas.
- Codecs de charge utile/résultat générés et schémas JSON orientés modèle.
- Un contrat dactylographié en exécution directe.
- Un client d’exécution généré avec une exécution par session et en une seule fois.
- Un chemin vers la planification basée sur un modèle, le streaming UIs et la composition des agents.
Pour la production, ajoutez le moteur Temporal pour la durabilité, un stockage unique du runtime appartenant à l’application, un stockage de mémoire appartenant au produit si nécessaire, Pulse pour le streaming distribué et le middleware de modèle pour les limites de débit du fournisseur. La conception Goa reste la source de vérité.
Prochaines étapes
| Guide | Ce que vous apprendrez |
|---|---|
| Référence DSL | Toutes les fonctions DSL : politiques, MCP, registres |
| Exécution | Boucle de planification/exécution, moteurs, magasins de mémoire |
| Jeux d’outils | Outils, transformations, exécuteurs basés sur des services |
| Composition d’agent | Analyse approfondie des modèles d’agent en tant qu’outil |
| Production | Configuration Temporal, streaming vers UIs, limitation de débit |