Minimal, copy‑paste runnable example to go from zero → talking agent. Keep your design in design/, never edit gen/.
- Go 1.24+
- Goa v3 CLI (
go install goa.design/goa/v3/cmd/goa@latest) - Temporal dev server (for workflow execution)
- Easiest: Docker one‑liner below, or use Temporalite
mkdir -p $GOPATH/src/example.com/quickstart && cd $_
go mod init example.com/quickstart
go get goa.design/goa/v3@latest
go get goa.design/goa-ai@latest
This declares one service (orchestrator) with a single agent (chat), a tiny
helper toolset, and a typed direct completion.
package design
import (
. "goa.design/goa/v3/dsl"
. "goa.design/goa-ai/dsl"
)
var _ = API("orchestrator", func() {})
// Input and output types with inline descriptions (required by this repo style)
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 DraftTaskStep = Type("DraftTaskStep", func() {
Attribute("title", String, "Short step title")
Example(map[string]any{"title": "Review the current launch checklist"})
Required("title")
})
var TaskDraft = Type("TaskDraft", func() {
Attribute("assistant_text", String, "Short explanation of the generated draft")
Attribute("name", String, "Task name")
Attribute("goal", String, "Outcome-style goal")
Attribute("steps", ArrayOf(DraftTaskStep), "Ordered draft steps")
Example(map[string]any{
"assistant_text": "Created a launch-readiness task draft.",
"name": "Prepare launch checklist",
"goal": "Confirm the service is ready to launch.",
"steps": []map[string]any{
{"title": "Review release notes and rollout scope"},
{"title": "Confirm dashboards and alerts are healthy"},
{"title": "Share the launch checklist with stakeholders"},
},
})
Required("assistant_text", "name", "goal", "steps")
})
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), MaxConsecutiveFailedToolCalls(1))
TimeBudget("15s")
})
})
})goa gen example.com/quickstart/design
goa example example.com/quickstart/designThis creates:
gen/- Generated code (never edit by hand)cmd/orchestrator/main.go- Runnable example using the bootstrapinternal/agents/bootstrap/bootstrap.go- Wires runtime and registers agentsinternal/agents/chat/planner/planner.go- Stub planner (edit to connect your LLM)gen/<service>/completions/- Generated typed direct-completion helpers
Tools are for callable capabilities. When you want the assistant to return a typed result directly, declare a service-owned completion:
var TaskDraft = Type("TaskDraft", func() {
Attribute("name", String, "Task name")
Attribute("goal", String, "Outcome-style goal")
Required("name", "goal")
})
var _ = Service("orchestrator", func() {
Completion("draft_task", "Produce a task draft directly", func() {
Return(TaskDraft)
})
})Completion names are part of the structured-output contract. They must be
1-64 ASCII characters, may contain letters, digits, _, and -, and must
start with a letter or digit.
Regeneration emits gen/orchestrator/completions/ with the result schema,
typed codecs, and generated helpers such as CompleteDraftTask(...),
StreamCompleteDraftTask(...), and DecodeDraftTaskChunk(...).
The unary helper issues a unary model request with provider-enforced structured
output and decodes the assistant response through the generated codec. The
streaming helper stays on the raw model.Streamer surface: completion_delta
chunks are preview-only, exactly one final completion chunk is canonical, and
DecodeDraftTaskChunk(...) decodes only that final payload. Generated
completion helpers reject tool-enabled requests and caller-supplied
StructuredOutput. Providers that do not implement structured output return
model.ErrStructuredOutputUnsupported.
go run ./cmd/orchestratorExpected output:
RunID: orchestrator-chat-...
Assistant: Hello from example planner.
Completion draft_task: ...
Completion delta draft_task: ...
Completion stream draft_task: ...
The generated example uses the in-memory engine, so no Temporal is needed for development.
For production, start Temporal and configure the runtime:
# Start Temporal dev server
docker run --rm -d --name temporal-dev -p 7233:7233 temporalio/auto-setup:latestThen modify the bootstrap to use the Temporal engine:
import (
"goa.design/goa-ai/runtime/agent/engine/temporal"
"go.temporal.io/sdk/client"
)
eng, _ := temporal.NewWorker(temporal.Options{
ClientOptions: &client.Options{
HostPort: "127.0.0.1:7233",
Namespace: "default",
},
WorkerOptions: temporal.WorkerOptions{
TaskQueue: "<service>_<agent>_workflow",
},
})
rt := agentsruntime.New(agentsruntime.WithEngine(eng))Edit internal/agents/chat/planner/planner.go to connect your LLM:
func (p *examplePlanner) PlanStart(ctx context.Context, in *planner.PlanInput) (*planner.PlanResult, error) {
// 1. Get LLM client from runtime
// mc, _ := in.Agent.ModelClient("openai")
// 2. Build prompt from in.Messages
// 3. Decide: call tools or give final response
return &planner.PlanResult{
FinalResponse: &planner.FinalResponse{
Message: &model.Message{
Role: model.ConversationRoleAssistant,
Parts: []model.Part{model.TextPart{Text: "Your response here"}},
},
},
}, nil
}goa example also generated an HTTP JSON‑RPC server under cmd/orchestrator.
- Start it:
go run ./cmd/orchestrator -debug - It mounts the MCP‑compatible JSON‑RPC API on POST
/rpc. - Try a simple RPC (replace the tool name with one from your design):
curl -s http://localhost:8080/rpc \
-H 'Content-Type: application/json' \
-d '{
"jsonrpc":"2.0",
"id":1,
"method":"tools/call",
"params":{
"name":"orchestrator.helpers.answer",
"arguments": {"question": "What is the capital of Japan?"}
}
}' | jq .Note: tool execution requires wiring executors. For a first run, the in‑process demo above is the simplest path. When you bind tools to service methods (BindTo in the design), goa example will scaffold executors you can fill in.
- Always change design in
design/*.gothen rungoa gen(andgoa exampleas needed). Never editgen/by hand. - Generated tool specs live under
gen/<svc>/agents/<agent>/specs/…with typed codecs. - Tool payload examples come from authored top-level Goa
Example(...)blocks. Generated specs keep both schema variants plus parsed example input so provider adapters can choose schema annotations or nativeinput_examples. - Policies and caps are enforced by the runtime during execution; keep planners small and declarative.