Démarrage rapide

Build a working AI agent in 10 minutes. Start with a stub, add streaming, validation, then connect a real LLM.

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 :

  1. Une conception Goa avec un agent, un outil typé et une complétion directe typée.
  2. Agent généré, ensemble d’outils, code de complétion et de câblage d’exécution.
  3. Un exemple d’échafaudage exécutable avec un planificateur de stub que vous pouvez remplacer par un planificateur basé sur un modèle.
  4. 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 :

  1. PlanStart reçoit les messages utilisateur initiaux.
  2. Le planificateur renvoie un FinalResponse, des appels d’outils ou une demande d’attente.
  3. 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.
  4. PlanResume reçoit les sorties d’outils visibles par le planificateur.
  5. 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

GuideCe que vous apprendrez
Référence DSLToutes les fonctions DSL : politiques, MCP, registres
ExécutionBoucle de planification/exécution, moteurs, magasins de mémoire
Jeux d’outilsOutils, transformations, exécuteurs basés sur des services
Composition d’agentAnalyse approfondie des modèles d’agent en tant qu’outil
ProductionConfiguration Temporal, streaming vers UIs, limitation de débit