Build a Go Coding Tutor with ADK-Go
Build a multi-agent Go tutor that pulls live data from GitHub repos.
Introduction
30 Second Summary
When you're learning a new programming language, the best examples come from real projects. But jumping between a chatbot and GitHub to find relevant code, issues, and patterns breaks your flow.
In this project, you will build a multi-agent coding tutor in Go that lives in your terminal and pulls live data from any public GitHub repository to teach you with real-world examples.
What You'll Build
You'll type a Go question into your terminal and watch your tutor reach into live GitHub repositories, pulling real issues, commits, and repo stats to back up its answer on the spot.
By the end of this project, you'll have:
- A terminal Go tutor you can ask any programming question, getting answers enriched with real code from public repositories.
- Custom Go tools that call the live GitHub API to pull repo stats, open issues, and recent commits from any public repository you name.
- Multi-agent collaboration where a coordinator agent delegates GitHub research to an autonomous specialist sub-agent and explains the results as teaching moments.
- Secret Mission: Add a Code Reviewer sub-agent that reads your local Go files and returns structured feedback with severity ratings.
Are there any prerequisites?
No prior Go experience is needed. This project walks you through installing Go, writing your first program, and building up to a multi-agent system from scratch.
Before We Start
Before we dive in, take a moment to commit to what you're about to build and why it matters. You're about to write a real Go program that talks to live APIs and orchestrates multiple AI agents in your terminal.
Install Go and Set Up Your Project
Every Go project starts with the Go toolchain and a project directory. Before you can write a single line of agent code, you need Go installed, a project scaffolded, and a free API key to power the Gemini model behind your tutor.
In this step, you will install Go 1.27 with Homebrew, scaffold a Go module, and let Go fetch ADK-Go as a dependency. By the end, you will be chatting with your first AI agent in the terminal and discovering exactly where it falls short.
In this step, get ready to:
- Install Go 1.27 with Homebrew and verify it is working.
- Create a Go project and fetch ADK-Go through Go modules.
- Run your first AI agent and discover its limitation.
Install Go and create the project
- Check whether Homebrew is ready by running this command:
brew --version
✔️ I see a Homebrew version
Homebrew is ready to install and update Go from the terminal.
ⓧ Command not found
Installing Homebrew changes files in its standard system location and may ask for your administrator password. The official script explains each change and pauses for your confirmation before it starts.
- Install Homebrew by running the official command:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
- Follow the installer prompts to confirm the changes.
- Run the commands shown under Next steps to add Homebrew to your shell.
- Close Terminal after the installer finishes.
- Open a new Terminal window through Spotlight.
- Confirm Homebrew is ready by running:
brew --version
Still seeing command not found?
Check that you ran every shell command printed under Next steps at the end of the installer. Help me fix the Homebrew path.
- Install the current stable Go release through Homebrew by running this command:
brew install go
- Check the installed Go version by running:
go version
✔️ I see go1.27.0 or higher
That is the toolchain ready. Go can now build your project and manage its dependencies.
ⓧ I see an older version
Your shell is finding an older Go installation. Homebrew can upgrade its Go formula and replace the outdated version on your path.
- Upgrade Homebrew's Go formula by running:
brew upgrade go
- Confirm the upgraded version by running:
go version
You should now see go1.27.0 or a newer version.
ⓧ Command not found
The Go formula may have installed correctly while your current shell still uses its previous command path.
- Close Terminal.
- Open a new Terminal window through Spotlight.
- Check the Go version again by running:
go version
Still cannot find Go?
Check that brew install go completed without an error. Help me find the Homebrew Go command.
Next, grab a free API key to power the Gemini model your tutor will use.
- Open https://aistudio.google.com/apikey in your browser.
- Sign in with your Google account.
- Click Create API key.
- Copy the key and keep it somewhere safe. You will paste it into a file in a moment.
What is Google AI Studio?
Google AI Studio is Google's web console for managing Gemini API keys. The free tier gives you enough requests to build this entire project at no cost.
Now create the project directory on your Desktop and initialize a Go module inside it.
- Move to your Desktop by running this command:
cd ~/Desktop
- Create your project folder and initialize the Go module by running these commands:
mkdir go-tutor && cd go-tutor
go mod init go-tutor
Your project needs the Gemini API key each time it starts. Choose a quick local file or an optional OpenBao workflow that keeps the key out of your project files.
Keep it simple with .env
This path gets your first agent running with the fewest moving parts. Your key stays in a local .env file that Git ignores.
- Store the key without showing it or saving it in your shell history by running these commands:
printf "Paste your Gemini API key: "
stty -echo
IFS= read -r GOOGLE_API_KEY
stty echo
printf "\n"
printf 'export GOOGLE_API_KEY="%s"\n' "$GOOGLE_API_KEY" > .env
unset GOOGLE_API_KEY
- Keep .env out of Git by creating .gitignore with this command:
printf '.env\n' > .gitignore
- Verify that the file exists and Git ignores it without printing the key by running:
test -s .env && grep -qx '.env' .gitignore && echo "Secret file ready and ignored by Git"
You should see Secret file ready and ignored by Git.
Did the safe check stay silent?
Make sure you are inside the go-tutor folder. Help me verify my .env file without revealing its secret.
Use OpenBao
This optional path practices retrieving a secret from a local secrets store. Your .env file contains only a retrieval command, never the Gemini API key.
- Install OpenBao through Homebrew by running this command. The download can take a minute on its first run:
brew install openbao
- Confirm the OpenBao CLI is available by running:
bao -h
You should see the OpenBao help output with its available commands.
Development mode is temporary
OpenBao development mode is insecure and stores all data in memory. Use it only for this local project because stopping the server erases the secret.
- Press Cmd+N in Terminal to open another window.
- Start the local OpenBao development server in the new window by running:
bao server -dev
The new window stays occupied while OpenBao listens on 127.0.0.1:8200. Keep it open while you work through the project.
- Return to the original Terminal window.
- Store the Gemini API key in OpenBao without showing it or saving it in your shell history by running:
printf "Paste your Gemini API key: "
stty -echo
IFS= read -r GOOGLE_API_KEY
stty echo
printf "\n"
printf '%s' "$GOOGLE_API_KEY" | bao kv put -mount=secret go-tutor GOOGLE_API_KEY=-
unset GOOGLE_API_KEY
- Create a secret-free .env shim and keep it out of Git by running:
cat > .env <<'EOF'
export GOOGLE_API_KEY="$(bao kv get -mount=secret -field=GOOGLE_API_KEY go-tutor)"
EOF
printf '.env\n' > .gitignore
How does OpenBao reach your Go code?
OpenBao sits outside your Go process. The .env shim turns one vault field into the environment variable that ADK-Go expects.
- Mount secret identifies the KV engine that stores the secret.
- Path go-tutor identifies the record inside that mount.
- Field GOOGLE_API_KEY selects only the Gemini key from that record.
- Environment variable GOOGLE_API_KEY carries the retrieved value inside the temporary agent process.
- Lookup os.Getenv("GOOGLE_API_KEY") in agent.go reads that value without placing the key in your source code.
The Go program does not import an OpenBao client library. The shell command performs the retrieval when the agent starts, which keeps secret storage separate from application code.
- Verify that OpenBao can supply the key without printing its value by running:
(source .env && test -n "$GOOGLE_API_KEY" && echo "OpenBao supplied the API key")
You should see OpenBao supplied the API key.
Cannot reach OpenBao?
Confirm the development server is still running in the other Terminal window. Help me connect the OpenBao CLI to my local development server.
What is a Go module?
A Go module is a collection of Go source files tracked by a go.mod file. The go.mod file records your project's name and the dependencies it needs. Every Go project starts with one.
- Confirm the module was created by running:
ls
Don't see go.mod or .env?
Make sure you ran both mkdir go-tutor && cd go-tutor and go mod init go-tutor from the same terminal session. If you opened a new tab or window between them, run cd ~/Desktop/go-tutor first to get back into the project folder.
Help me set up my Go project folder
Write your first agent and run it
With the project scaffolded, you can now write the Go code that creates an AI agent. The agent uses the ADK-Go framework to connect to the Gemini model and respond to your questions through an interactive terminal chat.
- Create a new file called agent.go in the go-tutor folder using your code editor.
- Add the following code to start your agent.go file:
package main
import (
"context"
"log"
"os"
"google.golang.org/genai"
"google.golang.org/adk/v2/agent"
"google.golang.org/adk/v2/agent/llmagent"
"google.golang.org/adk/v2/cmd/launcher"
"google.golang.org/adk/v2/cmd/launcher/full"
"google.golang.org/adk/v2/model/gemini"
)
func main() {
ctx := context.Background()
// Create a Gemini model using your API key
model, err := gemini.NewModel(ctx, "gemini-flash-latest", &genai.ClientConfig{
APIKey: os.Getenv("GOOGLE_API_KEY"),
})
if err != nil {
log.Fatalf("Failed to create model: %v", err)
}
What does this code do?
The import block adds gemini for model access, llmagent for agents, and launcher for terminal execution.
The gemini.NewModel call passes os.Getenv("GOOGLE_API_KEY") into genai.ClientConfig. The key stays out of agent.go.
If you chose OpenBao, the .env shim reads field GOOGLE_API_KEY from path go-tutor in the secret KV mount when the agent starts. It maps that value to the environment variable with the same name.
Go reads only the environment variable and never connects to OpenBao directly. gemini-flash-latest selects the current Gemini Flash model.
- Add this code directly below the model error check in agent.go:
// Create the tutor agent with a teaching instruction
tutor, err := llmagent.New(llmagent.Config{
Name: "go_tutor",
Model: model,
Description: "A Go programming tutor that answers Go questions.",
Instruction: `You are a friendly and knowledgeable Go programming tutor. You help people learn Go by answering questions clearly with examples. Always be encouraging and explain things step by step. Use code examples when helpful.`,
})
if err != nil {
log.Fatalf("Failed to create go_tutor: %v", err)
}
// Wire the agent through the launcher to start the CLI
config := &launcher.Config{
AgentLoader: agent.NewSingleLoader(tutor),
}
l := full.NewLauncher()
if err = l.Execute(ctx, config, os.Args[1:]); err != nil {
log.Fatalf("Run failed: %v\n\n%s", err, l.CommandLineSyntax())
}
}
What does this code do?
The llmagent.New call creates a tutor agent named go_tutor. The Instruction field tells the model how to behave: friendly, encouraging, with code examples.
The launcher.Config and full.NewLauncher() wire everything together and start an interactive chat session in your terminal. This is the ADK-Go launcher pattern, used in every ADK-Go project.
- Save agent.go.
✔️ Awesome, I've got everything!
Great. Make sure you have saved agent.go before moving on.
ⓧ I'd like to double check the full code
Scroll up and confirm your agent.go contains both code blocks in order:
- The first block: package main, the import block, func main(), and the gemini.NewModel call with its error check.
- The second block: the llmagent.New call creating the tutor, the launcher.Config, full.NewLauncher(), and the closing } of func main().
- Let Go fetch ADK-Go and record it in your module by running these commands:
go get google.golang.org/adk/v2
go mod tidy
- Confirm Go recorded ADK-Go in go.mod by running:
cat go.mod
You should see google.golang.org/adk/v2 in the dependency list. Go fetched the framework and pinned it to your project without cloning the repository.
Seeing download errors?
Check that your internet connection is active. If you see a timeout, try running go get google.golang.org/adk/v2 again. Go caches downloaded modules, so a retry often succeeds.
Help me fix my Go dependency download
Time to run the agent. Before you do, what do you think will happen when you start it?
- Load your API key and start the agent by running this command:
(source .env && exec go run agent.go)
You should see a User -> prompt appear in your terminal. This is the ADK-Go interactive console. You can type messages and the agent responds.
- Type explain what a struct is in Go and press Enter.
The tutor explains structs clearly, with code examples. It is pulling from its training data and doing a solid job on general Go questions.
Agent not starting?
If you see Failed to create model, confirm that your selected secret-storage path can load a non-empty key without displaying it.
- Check that the key is available by running:
(source .env && test -n "$GOOGLE_API_KEY" && echo "API key is available")
You should see API key is available. If you chose OpenBao and the check stays silent, confirm the development server is still running in the other Terminal window.
If you see a compile error, confirm both code blocks are in agent.go and the closing } of func main() is present at the end of the file.
Help me get my Go tutor agent running safely.
Notice the limitation
Your tutor handles general Go questions well. But what happens when you ask about something that exists on GitHub? Before you try it, take a guess: do you think the tutor can pull live data from a real GitHub repository?
- In the same chat session, type what are the open issues on google/adk-go? and press Enter.
- Try another one: type show me recent commits on google/go-github and press Enter.
The agent either makes up issue titles and commit messages, or admits it cannot access GitHub. It is stuck in its training data bubble with no way to reach the real world.
Why can't it access GitHub?
Right now the agent only has access to its training data. It has no tools, no API connections, and no way to fetch live information. When you ask about real repositories, it has to guess, and it gets things wrong.
This is the exact problem you will solve in the next step by giving the agent real GitHub tools.
- Type exit and press Enter to stop the agent.
Your tutor is working, but it is stuck in its training data bubble. In the next step, you will give it real superpowers by building GitHub tools in Go.
Build GitHub Tools in Go
Your tutor can answer Go questions, but it makes up answers when you ask about real GitHub repositories. That is because it has no way to reach the outside world.
In this step, you will write Go functions that call the real GitHub API using the go-github library. Then you will wire those functions into your agent as ADK tools so it can call them automatically.
In this step, get ready to:
- Write three Go functions that query the GitHub API for repo info, issues, and commits.
- Register all three functions as ADK tools on your tutor agent.
- Test the agent with a real GitHub question and see live data come back.
Add the go-github dependency and write tool functions
The go-github library is Google's official Go client for the GitHub API. It gives you strongly-typed Go structs for every GitHub resource, which is perfect for learning how structs work in Go.
- Install the go-github library by running this command:
go get github.com/google/go-github/v90
- Clean up your module file by running:
go mod tidy
What do these commands do?
go get downloads a Go package and adds it to your go.mod file. go mod tidy cleans up go.mod by removing any packages you no longer use and adding any you forgot.
Now you will create a new file for your GitHub tool functions. Each tool needs two things: a pair of Go structs defining its input and output, and a function that does the actual work.
- Create a new file called github_tools.go in your go-tutor folder.
- Add the package declaration, imports, and the input/output structs for the first tool by pasting the following code:
package main
import (
"context"
"fmt"
"github.com/google/go-github/v90/github"
"google.golang.org/adk/v2/agent"
)
// RepoInfoInput defines what the get_repo_info tool receives
type RepoInfoInput struct {
Owner string `json:"owner" jsonschema:"GitHub repository owner (e.g. google)"`
Repo string `json:"repo" jsonschema:"GitHub repository name (e.g. adk-go)"`
}
// RepoInfoOutput defines what the get_repo_info tool returns
type RepoInfoOutput struct {
Name string `json:"name"`
Description string `json:"description"`
Stars int `json:"stars"`
Language string `json:"language"`
OpenIssuesCount int `json:"open_issues_count"`
ForksCount int `json:"forks_count"`
HTMLURL string `json:"html_url"`
}
What does this code do?
Go structs define the shape of your data. The json tags tell Go how to convert struct fields to and from JSON. The jsonschema tags tell ADK what each field means, so the AI knows how to fill them in.
RepoInfoInput takes a repository owner and name. RepoInfoOutput returns metadata like stars, language, and open issues count.
- Add the getRepoInfo function below the RepoInfoOutput struct in github_tools.go:
func getRepoInfo(_ agent.Context, in RepoInfoInput) (RepoInfoOutput, error) {
// Create an unauthenticated GitHub client
client, err := github.NewClient()
if err != nil {
return RepoInfoOutput{}, fmt.Errorf("failed to create GitHub client: %w", err)
}
// Fetch the repository metadata from GitHub
repo, _, err := client.Repositories.Get(context.Background(), in.Owner, in.Repo)
if err != nil {
return RepoInfoOutput{}, fmt.Errorf("failed to get repo %s/%s: %w", in.Owner, in.Repo, err)
}
// Return a structured output with the fields we care about
return RepoInfoOutput{
Name: repo.GetName(),
Description: repo.GetDescription(),
Stars: repo.GetStargazersCount(),
Language: repo.GetLanguage(),
OpenIssuesCount: repo.GetOpenIssuesCount(),
ForksCount: repo.GetForksCount(),
HTMLURL: repo.GetHTMLURL(),
}, nil
}
What does this function do?
getRepoInfo follows the ADK tool signature: it takes an agent.Context (unused here, so we write _) and a typed input struct. It returns a typed output struct and an error.
github.NewClient() creates an unauthenticated client, which gives you 60 requests per hour. That is plenty for a tutorial. client.Repositories.Get() calls the GitHub API and returns the repo data as a Go struct.
- Save github_tools.go.
- Confirm the code compiles by running:
go build
If it compiles successfully, you see no output at all. Silence means success in Go.
Seeing a compile error?
- Check that the first line of github_tools.go says package main. Both files must be in the same package.
- Make sure you ran go get github.com/google/go-github/v90 before building. Without it, Go cannot find the import.
Help me debug a Go compile error in my github_tools.go file
Your first tool function compiles. Now add the second tool for listing issues.
- Add the input and output structs for the issues tool at the bottom of github_tools.go:
// ListIssuesInput defines what the list_issues tool receives
type ListIssuesInput struct {
Owner string `json:"owner" jsonschema:"GitHub repository owner"`
Repo string `json:"repo" jsonschema:"GitHub repository name"`
Label string `json:"label,omitempty" jsonschema:"Optional label to filter issues by"`
}
// IssueSummary holds a single issue's key fields
type IssueSummary struct {
Number int `json:"number"`
Title string `json:"title"`
State string `json:"state"`
Labels []string `json:"labels"`
}
// ListIssuesOutput wraps the list of issues and a total count
type ListIssuesOutput struct {
Issues []IssueSummary `json:"issues"`
Total int `json:"total"`
}
What is the omitempty tag?
The json:"label,omitempty" tag on the Label field tells Go to leave it out of the JSON if it is empty. This makes the label filter optional. The AI can call the tool with or without a label.
- Add the listIssues function below those structs:
func listIssues(_ agent.Context, in ListIssuesInput) (ListIssuesOutput, error) {
client, err := github.NewClient()
if err != nil {
return ListIssuesOutput{}, fmt.Errorf("failed to create GitHub client: %w", err)
}
// Configure options: open issues, max 10 results
opts := &github.IssueListByRepoOptions{
State: "open",
ListOptions: github.ListOptions{PerPage: 10},
}
if in.Label != "" {
opts.Labels = []string{in.Label}
}
issues, _, err := client.Issues.ListByRepo(context.Background(), in.Owner, in.Repo, opts)
if err != nil {
return ListIssuesOutput{}, fmt.Errorf("failed to list issues for %s/%s: %w", in.Owner, in.Repo, err)
}
// Build a summary, skipping pull requests
var summaries []IssueSummary
for _, issue := range issues {
if issue.PullRequestLinks != nil {
continue
}
var labels []string
for _, l := range issue.Labels {
labels = append(labels, l.GetName())
}
summaries = append(summaries, IssueSummary{
Number: issue.GetNumber(),
Title: issue.GetTitle(),
State: issue.GetState(),
Labels: labels,
})
}
return ListIssuesOutput{Issues: summaries, Total: len(summaries)}, nil
}
Why skip pull requests?
GitHub's API returns pull requests mixed into the issues endpoint. The if issue.PullRequestLinks != nil check filters them out so your tool only returns actual issues.
- Save github_tools.go and confirm it still compiles:
go build
No output means the second tool compiles cleanly. One more to go.
- Add the input and output structs for the commits tool at the bottom of github_tools.go:
// ListCommitsInput defines what the list_commits tool receives
type ListCommitsInput struct {
Owner string `json:"owner" jsonschema:"GitHub repository owner"`
Repo string `json:"repo" jsonschema:"GitHub repository name"`
}
// CommitSummary holds a single commit's key fields
type CommitSummary struct {
SHA string `json:"sha"`
Message string `json:"message"`
Author string `json:"author"`
Date string `json:"date"`
}
// ListCommitsOutput wraps the list of commits
type ListCommitsOutput struct {
Commits []CommitSummary `json:"commits"
}
- Add the listCommits function below those structs:
func listCommits(_ agent.Context, in ListCommitsInput) (ListCommitsOutput, error) {
client, err := github.NewClient()
if err != nil {
return ListCommitsOutput{}, fmt.Errorf("failed to create GitHub client: %w", err)
}
// Fetch the 5 most recent commits
opts := &github.CommitsListOptions{
ListOptions: github.ListOptions{PerPage: 5},
}
commits, _, err := client.Repositories.ListCommits(context.Background(), in.Owner, in.Repo, opts)
if err != nil {
return ListCommitsOutput{}, fmt.Errorf("failed to list commits for %s/%s: %w", in.Owner, in.Repo, err)
}
// Build a summary with short SHA, message, author, and date
var summaries []CommitSummary
for _, c := range commits {
summaries = append(summaries, CommitSummary{
SHA: c.GetSHA()[:7],
Message: c.GetCommit().GetMessage(),
Author: c.GetCommit().GetAuthor().GetName(),
Date: c.GetCommit().GetAuthor().GetDate().Format("2006-01-02"),
})
}
return ListCommitsOutput{Commits: summaries}, nil
}
What is the date format string?
Go uses a unique approach to date formatting. Instead of symbols like YYYY-MM-DD, Go uses a reference date: January 2, 2006 at 3:04:05 PM. So "2006-01-02" means year-month-day.
- Save github_tools.go and verify all three tools compile:
go build
No output again. All three GitHub tools compile successfully.
Seeing an error about undefined types?
- Make sure every struct is defined above the function that uses it. Go reads top to bottom.
- Check that the closing brace } for each struct and function is present. A missing brace throws confusing errors.
Help me fix a Go compile error in my github_tools.go
\u2714\uFE0F Awesome, I've got everything!
Great. Double check you have saved github_tools.go.
\u24E7 I'd like to double check the full code
Your github_tools.go file should contain six chunks in this order. Scroll up and confirm each one is present:
- Package declaration and imports (context, fmt, go-github, adk/v2/agent).
- RepoInfoInput and RepoInfoOutput structs, then the getRepoInfo function.
- ListIssuesInput, IssueSummary, and ListIssuesOutput structs, then the listIssues function.
- ListCommitsInput, CommitSummary, and ListCommitsOutput structs, then the listCommits function.
Register tools with the agent
Your tool functions exist, but the agent does not know about them yet. You need to wrap each function as an ADK tool and add them to the agent's tool list.
- In agent.go, find the import block. It currently looks like this:
"google.golang.org/adk/v2/model/gemini"
)
- Add two new import lines above the closing parenthesis, so the end of your import block looks like this:
"google.golang.org/adk/v2/model/gemini"
"google.golang.org/adk/v2/tool"
"google.golang.org/adk/v2/tool/functiontool"
)
These imports bring in the tool interface (so you can list tools on your agent) and the functiontool package (so you can wrap Go functions as tools).
- In agent.go, find the closing brace of the error check after gemini.NewModel. It looks like this:
if err != nil {
log.Fatalf("Failed to create model: %v", err)
}
- Add the following code below that error check to wrap all three tool functions:
// ----- GitHub tools -----
repoInfoTool, err := functiontool.New(functiontool.Config{
Name: "get_repo_info",
Description: "Get metadata about a GitHub repository including stars, description, language, and open issues count.",
}, getRepoInfo)
if err != nil {
log.Fatalf("Failed to create get_repo_info tool: %v", err)
}
listIssuesTool, err := functiontool.New(functiontool.Config{
Name: "list_issues",
Description: "List open issues for a GitHub repository, optionally filtered by label.",
}, listIssues)
if err != nil {
log.Fatalf("Failed to create list_issues tool: %v", err)
}
listCommitsTool, err := functiontool.New(functiontool.Config{
Name: "list_commits",
Description: "List recent commits for a GitHub repository.",
}, listCommits)
if err != nil {
log.Fatalf("Failed to create list_commits tool: %v", err)
}
What does functiontool.New do?
functiontool.New takes a config (name and description) and your Go function, then returns an ADK tool object. The name is what the AI sees when deciding which tool to call. The description tells the AI when to use it.
ADK automatically inspects your function's input struct to build a parameter schema. That is why the jsonschema tags you wrote earlier matter.
- Now find the existing llmagent.New call in agent.go. It currently starts with:
tutor, err := llmagent.New(llmagent.Config{
Name: "go_tutor",
Model: model,
Description: "A Go programming tutor that answers Go questions.",
Instruction: `You are a friendly and knowledgeable Go programming tutor. You help people learn Go by answering questions clearly with examples. Always be encouraging and explain things step by step. Use code examples when helpful.`,
})
- Replace that entire block (from tutor, err := through the closing })) with this updated version that includes the tools and a smarter instruction:
tutor, err := llmagent.New(llmagent.Config{
Name: "go_tutor",
Model: model,
Description: "A Go programming tutor that can look up real GitHub data to enrich its teaching.",
Tools: []tool.Tool{
repoInfoTool,
listIssuesTool,
listCommitsTool,
},
Instruction: `You are a friendly and knowledgeable Go programming tutor. You help people learn Go by answering questions clearly with examples.
When someone asks about a specific GitHub repository (issues, commits, repo details), use your GitHub tools to get real data and explain the results in a teaching context.
For general Go questions (syntax, concepts, best practices), answer directly from your knowledge.
Always be encouraging and explain things step by step. Use code examples when helpful.`,
})
What changed in the agent config?
You added a Tools slice that gives the agent access to all three GitHub functions. You also updated the Instruction to tell the agent when to use them: GitHub questions get tools, general Go questions get direct answers.
- Save agent.go.
- Clean up your dependencies by running:
go mod tidy
\u2714\uFE0F Awesome, I've got everything!
Great. Double check you have saved agent.go.
\u24E7 I'd like to double check the full code
Your agent.go file should contain these sections in order. Scroll up and confirm each one is present:
- Import block with genai, agent, llmagent, launcher, full, gemini, tool, and functiontool.
- Model creation with gemini.NewModel.
- Three functiontool.New calls creating repoInfoTool, listIssuesTool, and listCommitsTool.
- The llmagent.New call with Tools: []tool.Tool{...} listing all three tools.
- Launcher config and full.NewLauncher() at the bottom.
Time to see your tools in action. Before you run the agent, think about what you expect to happen.
If you ask "tell me about the google/adk-go repo", do you think the agent will call your getRepoInfo tool and return real numbers? Or will it still make things up?
- Load your API key and run the agent:
source .env && go run agent.go
- Type this question into the chat:
tell me about the google/adk-go repo
The agent calls your getRepoInfo tool and returns real data: the repo's star count, description, language, and open issues count. These are live numbers from GitHub, not fabricated answers.
Agent not returning real data?
- Make sure you saved both agent.go and github_tools.go before running.
- Check that the Tools slice in the llmagent.New call includes all three tool variables.
- Run go build first to check for compile errors. Fix any errors before running go run agent.go.
Help me debug why my Go tutor agent is not calling its GitHub tools
Your tutor now pulls live data from any public GitHub repo. But you might notice a problem if you keep chatting. Try asking a simple Go question like "explain goroutines" and watch whether the agent tries to call a GitHub tool for it. Next up, you will split the agent into a smarter multi-agent system that routes questions to the right place.
Split Into a Multi-Agent System
Your Go tutor can now pull live data from any public GitHub repo. There is a problem, though. The single agent handles both tutoring AND GitHub queries, and its instruction prompt is getting bloated.
You may have noticed it sometimes calls GitHub tools for simple Go questions, or dumps raw data without explaining it. The fix is to split into two agents: a coordinator that teaches, and a specialist that researches. In ADK-Go, this is called multi-agent delegation.
In this step, get ready to:
- Create a GitHub Researcher sub-agent with single-turn mode.
- Rewire the coordinator to delegate GitHub questions to the researcher.
- Test that delegation routes correctly for different question types.
Create the GitHub Researcher sub-agent
The idea behind multi-agent systems is specialization. Instead of one overloaded agent doing everything, you give each agent a focused role and let a coordinator route requests to the right specialist.
The GitHub Researcher will own the three GitHub tools and run in single-turn mode. That means it receives a task, autonomously calls the tools it needs, and returns results to the parent agent without chatting with the user directly.
- In agent.go, find the line that says repoInfoTool, err := functiontool.New(.
- Add this comment on the line directly above it, to organize your code as it grows:
// ----- GitHub tools -----
- Now scroll down to the line that says tutor, err := llmagent.New(.
- Add the following code directly ABOVE the tutor block, to create the GitHub Researcher sub-agent:
// ----- GitHub Researcher sub-agent (single_turn) -----
githubResearcher, err := llmagent.New(llmagent.Config{
Name: "github_researcher",
Model: model,
Description: "Researches public GitHub repositories for repo info, issues, and recent commits.",
Mode: llmagent.ModeSingleTurn,
Tools: []tool.Tool{
repoInfoTool,
listIssuesTool,
listCommitsTool,
},
Instruction: `You are a GitHub research assistant. When asked about a repository, use your tools to gather data.
Use get_repo_info to get repository metadata.
Use list_issues to find open issues, optionally filtered by label.
Use list_commits to see recent activity.
Summarize your findings clearly and concisely. Always include specific numbers, names, and dates.`,
})
if err != nil {
log.Fatalf("Failed to create github_researcher: %v", err)
}
What does this code do?
- llmagent.ModeSingleTurn tells ADK-Go that this agent runs autonomously. It receives a task, calls its tools, and returns results to the parent. It never talks directly to the user.
- The three GitHub tools (repoInfoTool, listIssuesTool, listCommitsTool) are now assigned to this specialist agent instead of the tutor.
- The focused instruction keeps the researcher scoped to data gathering. It never tries to teach.
💡 Why split into multiple agents?
A single agent with a long, multi-purpose instruction struggles to decide when to use tools vs. answer directly. Splitting into focused agents gives each one a smaller instruction and a clear responsibility. The coordinator teaches. The researcher fetches data.
ADK-Go handles the routing automatically. When the coordinator mentions the sub-agent by name in its instruction, ADK auto-injects a delegation tool so the coordinator can hand off requests.
The code will not compile yet because githubResearcher is declared but not used anywhere. The next substep connects it to the coordinator.
Rewire the coordinator to delegate
Now you need to update the tutor so it stops holding the GitHub tools directly and instead delegates to the researcher.
- In agent.go, find the existing tutor block. It currently looks like this:
tutor, err := llmagent.New(llmagent.Config{
Name: "go_tutor",
Model: model,
Description: "A Go programming tutor that can look up real GitHub data to enrich its teaching.",
Tools: []tool.Tool{
repoInfoTool,
listIssuesTool,
listCommitsTool,
},
Instruction: `You are a friendly and knowledgeable Go programming tutor...`,
})
if err != nil {
log.Fatalf("Failed to create go_tutor: %v", err)
}
- Replace the entire tutor block (from tutor, err := through the log.Fatalf closing brace) with this new version:
// ----- Coordinator (tutor) agent -----
tutor, err := llmagent.New(llmagent.Config{
Name: "go_tutor",
Model: model,
Description: "A Go programming tutor that can look up real GitHub data to enrich its teaching.",
SubAgents: []agent.Agent{
githubResearcher,
},
Instruction: `You are a friendly and knowledgeable Go programming tutor. You help people learn Go by answering questions clearly with examples.
When someone asks about a specific GitHub repository (issues, commits, repo details, examples from real projects), delegate to github_researcher and then explain the results in a teaching context. For example, if they ask about open issues, explain what the issues mean and how they relate to Go concepts.
For general Go questions (syntax, concepts, best practices), answer directly from your knowledge. Do not call github_researcher for general Go questions.
Always be encouraging and explain things step by step. Use code examples when helpful.`,
})
if err != nil {
log.Fatalf("Failed to create go_tutor: %v", err)
}
What changed?
- Tools: []tool.Tool{...} is gone. The coordinator no longer holds any GitHub tools directly.
- SubAgents: []agent.Agent{githubResearcher} replaces it. ADK-Go automatically creates a delegation tool so the coordinator can hand off requests to github_researcher by name.
- The instruction now explicitly tells the coordinator to delegate GitHub questions to github_researcher and teach using the results. General Go questions stay with the coordinator.
- Save agent.go.
✔️ Awesome, I've got everything!
Great. Make sure you have saved agent.go before running it.
ⓧ I'd like to double check the full code
Your agent.go should have these sections in order. Scroll up and confirm each one is present:
- Package declaration and imports (unchanged from before).
- Model creation with gemini.NewModel (unchanged).
- // ----- GitHub tools ----- comment, then the three tool definitions (repoInfoTool, listIssuesTool, listCommitsTool) unchanged.
- // ----- GitHub Researcher sub-agent (single_turn) ----- comment, then the githubResearcher definition with ModeSingleTurn and all three tools.
- // ----- Coordinator (tutor) agent ----- comment, then the tutor definition with SubAgents (no Tools).
- Launcher config and l.Execute (unchanged).
github_tools.go is completely unchanged in this step.
Test the multi-agent delegation
Time to see if the split works. The coordinator should delegate GitHub questions to the researcher and answer general Go questions on its own.
- Start your tutor by running:
source .env && go run agent.go
Before you test, think about what you expect to happen. If you ask "what are the newest issues on google/go-github?", do you think the coordinator will answer directly or delegate to the researcher?
- Type this question into the tutor:
what are the newest issues on google/go-github?
The coordinator delegates to github_researcher, which autonomously calls your listIssues tool and returns findings. The coordinator then explains the results in a teaching context, connecting the issues to Go concepts.
- Now test the other path. Ask a general Go question:
explain goroutines in Go
This time the coordinator answers directly from its own knowledge. No delegation happens because the question has nothing to do with a specific GitHub repository.
Agent not delegating or returning errors?
- Make sure you removed the Tools slice from the tutor config and replaced it with SubAgents. If both are present, the coordinator may try to call tools directly.
- Check that the coordinator's instruction mentions github_researcher by name. ADK-Go uses that name to auto-inject the delegation tool.
- If you see a compilation error about an unused variable, make sure githubResearcher appears inside the tutor's SubAgents slice.
Still stuck? Ask for help.
Your tutor now has a clean separation of concerns. The coordinator teaches, and the researcher fetches data. Next up, you will tackle the Secret Mission and add a second specialist agent that can review your local Go files.
Secret mission
Add a Code Reviewer Sub-Agent
Your tutor can answer Go questions and research GitHub repos. But what if it could also review your own code? Add a Code Reviewer sub-agent that reads local Go files and returns structured feedback with specific suggestions, using a more advanced ADK-Go pattern: task mode with structured input and output schemas.
Clean Up Your Resources
Clean Up Your Resources
This project runs entirely locally and uses only free services. There are no ongoing costs from the Gemini API free tier or the unauthenticated GitHub API.
Decide whether to keep your resources running, pause them to come back later, or delete them entirely.
Resources you used:
- The go-tutor project directory on your Desktop, including agent.go, github_tools.go, file_tools.go, go.mod, .env, and .gitignore.
- The optional OpenBao development server, if you chose the OpenBao secrets path.
- Your Gemini API key in Google AI Studio.
Keep everything running
No action needed. Choose this if you are still actively building or want to keep experimenting with your Go tutor.
- Keep the go-tutor folder on your Desktop.
- Keep the OpenBao development server running if you chose the OpenBao secrets path.
- Start your tutor again by running these commands:
cd ~/Desktop/go-tutor
source .env && go run agent.go
You should see the tutor prompt in your terminal.
Did OpenBao stop earlier?
Restart the OpenBao development server before running the tutor if you chose that path. Help me restore my OpenBao-backed API key.
Pause - I'll come back to this later
Shut down running processes to free up memory while keeping your project files.
- Press Ctrl+C in the tutor terminal to stop the agent.
- Press Ctrl+C in the OpenBao server terminal if you chose the OpenBao path.
Stopping the OpenBao development server erases its in-memory secret. You need to store the Gemini API key again when you return.
- Keep the go-tutor folder on your Desktop until you continue.
Delete - I don't want to use this again
Deleting the project removes your local code and secret file. Stop any running processes first so no terminal remains attached to files that no longer exist.
- Press Ctrl+C in the tutor terminal if the agent is running.
- Press Ctrl+C in the OpenBao terminal if its development server is running.
macOS
- Delete the go-tutor folder from your Desktop by running:
rm -rf ~/Desktop/go-tutor
- Confirm the folder is gone by running:
test ! -d ~/Desktop/go-tutor && echo "Project folder deleted"
You should see Project folder deleted.
Windows
- Delete the go-tutor folder from your Desktop by running:
Remove-Item -Recurse -Force "$HOME\Desktop\go-tutor"
- Confirm the folder is gone by running:
Test-Path "$HOME\Desktop\go-tutor"
You should see False.
- Open Google AI Studio's API keys page in your browser.
- Find the Gemini API key you created for this project.
- Click the trash icon beside the key.
- Confirm the deletion when prompted.
Your local project, optional in-memory OpenBao secret, and Gemini API key are now removed.
Nice Work!
Nice Work!
You just built a multi-agent AI coding tutor in Go from scratch. Your tutor answers programming questions, pulls live data from any public GitHub repository, and delegates tasks to specialist sub-agents.
You've learned how to:
- Installed Go from scratch and learned core fundamentals like modules, structs, functions, and error handling by building a real application.
- Built custom ADK-Go tools that make live GitHub API calls using the go-github library.
- Created a multi-agent system with a coordinator that delegates to a GitHub Researcher sub-agent in single-turn mode.
- Completed an optional Secret Mission to push your multi-agent skills further.
Ready to quiz yourself?