stemedb/docs/references/go-adk/agent-multi-chatters.md
jordan 3cfaa1e1d3 feat: Complete Phase 1 (The Spine) - storage foundation
Phase 1 delivers the complete durability and storage layer:

- WAL with crash recovery: Append-only journal with BLAKE3 checksums,
  fsync guarantees, and proper seek-to-EOF on reopen
- Storage engine: sled-backed KVStore with scan_prefix for range queries
- Content-addressed storage: H:{hash}, V:{hash}, E:{hash} key patterns
- Ingestor: Background worker tailing WAL, writing to KV with 8-byte
  aligned record headers for rkyv zero-copy deserialization
- Comprehensive tests: 31 tests covering crash recovery, round-trips,
  and multi-cycle durability

New crates: stemedb-wal, stemedb-storage, stemedb-ingest

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-31 14:15:34 -07:00

17 KiB

Building a Multi-Agent Chat Room with ADK-Go

This guide shows how to create a chat room where multiple AI agents with different perspectives and personalities discuss topics together, providing users with diverse viewpoints.


Overview

The multi-agent chat room simulates a discussion panel:

┌─────────────────────────────────────────────────────────────────┐
│                     DISCUSSION MODERATOR                        │
│         Orchestrates conversation, ensures balance              │
├────────────┬────────────┬────────────┬────────────┬────────────┤
│  OPTIMIST  │  SKEPTIC   │  ANALYST   │  CREATIVE  │  PRAGMATIC │
│            │            │            │            │            │
│ Sees       │ Questions  │ Examines   │ Explores   │ Focuses on │
│ potential  │ assumptions│ data/facts │ new ideas  │ feasibility│
└────────────┴────────────┴────────────┴────────────┴────────────┘

Project Setup

mkdir multi-agent-chatroom && cd multi-agent-chatroom
go mod init chatroom
go get google.golang.org/adk

Core Implementation

Agent Personas

package main

import (
	"context"
	"fmt"
	"log"
	"os"
	"strings"

	"google.golang.org/adk/agent"
	"google.golang.org/adk/agent/llmagent"
	"google.golang.org/adk/agent/loopagent"
	"google.golang.org/adk/agent/sequentialagent"
	"google.golang.org/adk/cmd/launcher/adk"
	"google.golang.org/adk/cmd/launcher/full"
	"google.golang.org/adk/model/gemini"
	"google.golang.org/adk/server/restapi/services"
	"google.golang.org/adk/tool"
	"google.golang.org/adk/tool/functiontool"
	"google.golang.org/adk/tool/geminitool"
	"google.golang.org/genai"
)

// Persona definitions
type Persona struct {
	Name        string
	Role        string
	Instruction string
	Style       string
}

var personas = []Persona{
	{
		Name: "optimist",
		Role: "The Optimist",
		Instruction: `You are an optimistic thinker who sees potential and opportunities.

PERSPECTIVE:
- Focus on positive outcomes and possibilities
- Highlight benefits and advantages
- Encourage bold thinking and innovation
- Find silver linings in challenges

STYLE:
- Enthusiastic but not naive
- Back optimism with reasons
- Acknowledge risks but emphasize how they can be overcome
- Use phrases like "The exciting part is...", "This opens up...", "Imagine if..."`,
	},
	{
		Name: "skeptic",
		Role: "The Skeptic",
		Instruction: `You are a constructive skeptic who questions assumptions.

PERSPECTIVE:
- Identify potential problems and risks
- Question unstated assumptions
- Play devil's advocate
- Demand evidence for claims

STYLE:
- Critical but not cynical
- Ask probing questions
- Point out what could go wrong
- Use phrases like "But have we considered...", "What evidence shows...", "The risk is..."`,
	},
	{
		Name: "analyst",
		Role: "The Analyst",
		Instruction: `You are a data-driven analyst who examines facts objectively.

PERSPECTIVE:
- Focus on data, statistics, and evidence
- Compare with historical examples
- Identify patterns and trends
- Quantify when possible

STYLE:
- Objective and methodical
- Reference specific numbers and studies
- Present multiple scenarios
- Use phrases like "The data suggests...", "Historically...", "Studies show..."`,
	},
	{
		Name: "creative",
		Role: "The Creative",
		Instruction: `You are a creative thinker who explores unconventional ideas.

PERSPECTIVE:
- Think outside the box
- Make unexpected connections
- Propose innovative solutions
- Challenge conventional wisdom

STYLE:
- Imaginative and bold
- Use analogies and metaphors
- Propose "what if" scenarios
- Use phrases like "What if we...", "Here's a wild idea...", "Imagine combining..."`,
	},
	{
		Name: "pragmatist",
		Role: "The Pragmatist",
		Instruction: `You are a practical thinker focused on real-world implementation.

PERSPECTIVE:
- Focus on feasibility and execution
- Consider resources, time, and constraints
- Identify concrete next steps
- Balance idealism with reality

STYLE:
- Grounded and action-oriented
- Ask "how" questions
- Propose specific steps
- Use phrases like "Practically speaking...", "To make this work...", "The first step would be..."`,
	},
}

func main() {
	ctx := context.Background()

	model, err := gemini.NewModel(ctx, "gemini-3-flash-preview", &genai.ClientConfig{
		APIKey: os.Getenv("GOOGLE_API_KEY"),
	})
	if err != nil {
		log.Fatal(err)
	}

	chatroom, err := buildChatRoom(model)
	if err != nil {
		log.Fatal(err)
	}

	l := full.NewLauncher()
	cfg := &adk.Config{AgentLoader: services.NewSingleAgentLoader(chatroom)}
	if err := l.Execute(ctx, cfg, os.Args[1:]); err != nil {
		log.Fatal(err)
	}
}

Building the Chat Room

func buildChatRoom(model *gemini.Model) (agent.Agent, error) {
	// Create tools for agent interaction
	speakTool, err := functiontool.New(
		functiontool.Config{
			Name:        "speak",
			Description: "Add your contribution to the discussion",
		},
		speak,
	)
	if err != nil {
		return nil, err
	}

	respondToTool, err := functiontool.New(
		functiontool.Config{
			Name:        "respond_to",
			Description: "Respond directly to another participant's point",
		},
		respondTo,
	)
	if err != nil {
		return nil, err
	}

	// Create participant agents
	var participants []agent.Agent
	for _, p := range personas {
		participant, err := createParticipant(model, p, []tool.Tool{speakTool, respondToTool})
		if err != nil {
			return nil, err
		}
		participants = append(participants, participant)
	}

	// Create moderator that orchestrates the discussion
	moderator, err := createModerator(model, participants)
	if err != nil {
		return nil, err
	}

	return moderator, nil
}

func createParticipant(model *gemini.Model, p Persona, tools []tool.Tool) (agent.Agent, error) {
	instruction := fmt.Sprintf(`You are %s in a multi-perspective discussion panel.

%s

DISCUSSION CONTEXT:
Topic: {discussion_topic}

Previous contributions:
{discussion_history}

YOUR TASK:
Provide your unique perspective on the topic. Consider what others have said and either:
1. Build on their points with your perspective
2. Respectfully challenge or question their views
3. Introduce a new angle they haven't considered

Keep your response focused (2-4 sentences). Use the speak tool to contribute.
If directly responding to someone, use respond_to tool.`, p.Role, p.Instruction)

	return llmagent.New(llmagent.Config{
		Name:        p.Name,
		Model:       model,
		Description: fmt.Sprintf("%s - %s", p.Role, p.Style),
		Instruction: instruction,
		Tools:       tools,
		OutputKey:   fmt.Sprintf("%s_contribution", p.Name),
	})
}

func createModerator(model *gemini.Model, participants []agent.Agent) (agent.Agent, error) {
	// Opening agent - frames the discussion
	opener, err := llmagent.New(llmagent.Config{
		Name:  "opener",
		Model: model,
		Instruction: `You are a discussion moderator. Frame the topic for the panel.

Topic from user: {user_input}

Create a brief, engaging introduction that:
1. Restates the topic clearly
2. Highlights why it's worth discussing
3. Poses 1-2 key questions for the panel

Keep it to 2-3 sentences. Be neutral and inviting.`,
		OutputKey: "discussion_topic",
	})
	if err != nil {
		return nil, err
	}

	// Discussion round - all participants contribute
	discussionRound, err := createDiscussionRound(model, participants)
	if err != nil {
		return nil, err
	}

	// Synthesizer - summarizes and identifies key insights
	synthesizer, err := llmagent.New(llmagent.Config{
		Name:  "synthesizer",
		Model: model,
		Instruction: `You are summarizing a multi-perspective discussion.

TOPIC: {discussion_topic}

CONTRIBUTIONS:
{discussion_history}

Create a balanced summary that:
1. Highlights the key points from each perspective
2. Notes areas of agreement and disagreement
3. Identifies the most compelling insights
4. Suggests questions for further exploration

Be fair to all viewpoints. Present the synthesis conversationally.`,
		OutputKey: "synthesis",
	})
	if err != nil {
		return nil, err
	}

	// Build full pipeline: Open → Discuss (loop) → Synthesize
	pipeline, err := sequentialagent.New(sequentialagent.Config{
		AgentConfig: agent.Config{
			Name:        "discussion_chatroom",
			Description: "Multi-perspective discussion panel",
			SubAgents:   []agent.Agent{opener, discussionRound, synthesizer},
		},
	})

	return pipeline, err
}

func createDiscussionRound(model *gemini.Model, participants []agent.Agent) (agent.Agent, error) {
	// History tracker - runs before each participant
	historyUpdater, err := llmagent.New(llmagent.Config{
		Name:  "history_updater",
		Model: model,
		Instruction: `Update the discussion history with recent contributions.

Current history:
{discussion_history}

New contributions to add:
- Optimist: {optimist_contribution}
- Skeptic: {skeptic_contribution}
- Analyst: {analyst_contribution}
- Creative: {creative_contribution}
- Pragmatist: {pragmatist_contribution}

Output the combined, formatted discussion history.`,
		OutputKey: "discussion_history",
	})
	if err != nil {
		return nil, err
	}

	// Build round: each participant speaks, then history updates
	roundAgents := append(participants, historyUpdater)

	// Create a sequential round
	round, err := sequentialagent.New(sequentialagent.Config{
		AgentConfig: agent.Config{
			Name:      "discussion_round",
			SubAgents: roundAgents,
		},
	})
	if err != nil {
		return nil, err
	}

	// Loop for multiple rounds of discussion
	multiRound, err := loopagent.New(loopagent.Config{
		AgentConfig: agent.Config{
			Name:      "multi_round_discussion",
			SubAgents: []agent.Agent{round},
		},
		MaxIterations: 2, // Two rounds of discussion
	})

	return multiRound, err
}

Speaking Tools

type SpeakInput struct {
	Message string `json:"message" jsonschema:"Your contribution to the discussion"`
}

type SpeakOutput struct {
	Delivered bool   `json:"delivered"`
	Speaker   string `json:"speaker"`
}

func speak(ctx tool.Context, input SpeakInput) SpeakOutput {
	// Get the current agent's name from context
	agentName := "participant"

	// Store in discussion log
	state := ctx.Session().State()
	var log []string
	if existing := state.Get("temp:discussion_log"); existing != nil {
		if l, ok := existing.([]string); ok {
			log = l
		}
	}

	entry := fmt.Sprintf("[%s]: %s", strings.ToUpper(agentName), input.Message)
	log = append(log, entry)
	state.Set("temp:discussion_log", log)

	return SpeakOutput{Delivered: true, Speaker: agentName}
}

type RespondToInput struct {
	Target  string `json:"target" jsonschema:"Who you're responding to (optimist, skeptic, analyst, creative, pragmatist)"`
	Message string `json:"message" jsonschema:"Your response"`
}

type RespondToOutput struct {
	Delivered bool `json:"delivered"`
}

func respondTo(ctx tool.Context, input RespondToInput) RespondToOutput {
	state := ctx.Session().State()
	var log []string
	if existing := state.Get("temp:discussion_log"); existing != nil {
		if l, ok := existing.([]string); ok {
			log = l
		}
	}

	entry := fmt.Sprintf("[RESPONSE to %s]: %s", strings.ToUpper(input.Target), input.Message)
	log = append(log, entry)
	state.Set("temp:discussion_log", log)

	return RespondToOutput{Delivered: true}
}

Alternative: Real-Time Debate Format

For more dynamic back-and-forth:

func buildDebateRoom(model *gemini.Model) (agent.Agent, error) {
	// Pro side
	proDebater, _ := llmagent.New(llmagent.Config{
		Name:  "pro_debater",
		Model: model,
		Instruction: `You are arguing IN FAVOR of the proposition.

Proposition: {proposition}
Opponent's last argument: {con_argument}

Build a compelling case FOR the proposition. If responding to opponent:
- Acknowledge their point
- Counter with evidence or logic
- Strengthen your position

Be persuasive but fair. 2-3 sentences max.`,
		OutputKey: "pro_argument",
	})

	// Con side
	conDebater, _ := llmagent.New(llmagent.Config{
		Name:  "con_debater",
		Model: model,
		Instruction: `You are arguing AGAINST the proposition.

Proposition: {proposition}
Opponent's last argument: {pro_argument}

Build a compelling case AGAINST the proposition. If responding to opponent:
- Acknowledge their point
- Counter with evidence or logic
- Strengthen your position

Be persuasive but fair. 2-3 sentences max.`,
		OutputKey: "con_argument",
	})

	// Debate round
	round, _ := sequentialagent.New(sequentialagent.Config{
		AgentConfig: agent.Config{
			Name:      "debate_round",
			SubAgents: []agent.Agent{proDebater, conDebater},
		},
	})

	// Multiple rounds
	debate, _ := loopagent.New(loopagent.Config{
		AgentConfig: agent.Config{
			Name:      "debate",
			SubAgents: []agent.Agent{round},
		},
		MaxIterations: 3,
	})

	// Judge
	judge, _ := llmagent.New(llmagent.Config{
		Name:  "judge",
		Model: model,
		Instruction: `You are an impartial debate judge.

Proposition: {proposition}

PRO arguments: {pro_argument}
CON arguments: {con_argument}

Evaluate the debate:
1. Strongest argument from each side
2. Key points of clash
3. Which side was more persuasive and why
4. What was missing from both sides

Be balanced and specific in your assessment.`,
		OutputKey: "verdict",
	})

	// Full debate pipeline
	return sequentialagent.New(sequentialagent.Config{
		AgentConfig: agent.Config{
			Name:      "debate_room",
			SubAgents: []agent.Agent{debate, judge},
		},
	})
}

Adding Research Capability

Let agents search for evidence during discussion:

func createResearchParticipant(model *gemini.Model, p Persona) (agent.Agent, error) {
	instruction := fmt.Sprintf(`You are %s in a research-backed discussion.

%s

Topic: {discussion_topic}
Previous points: {discussion_history}

You have access to web search. Use it to:
- Find evidence supporting your perspective
- Discover relevant statistics or studies
- Check facts before stating them

Provide your perspective backed by what you find. Cite sources.`, p.Role, p.Instruction)

	return llmagent.New(llmagent.Config{
		Name:        p.Name,
		Model:       model,
		Description: p.Role,
		Instruction: instruction,
		Tools: []tool.Tool{
			geminitool.GoogleSearch{},
		},
		OutputKey: fmt.Sprintf("%s_contribution", p.Name),
	})
}

Running the Chat Room

# Console mode
go run main.go

# Example prompts:
# > Should companies require employees to return to the office?
# > Is social media beneficial or harmful to society?
# > Should AI-generated art be considered "real" art?
# > What's the best approach to addressing climate change?

# Web UI mode
go run main.go web api webui

Sample Output

User: Should universities require students to take a gap year before college?

Moderator: Today we're discussing whether universities should mandate a gap year before enrollment. This touches on questions of maturity, experience, and educational readiness. What perspectives should we consider?

Optimist: A gap year could be transformative! Students return with real-world experience, clearer goals, and renewed motivation. Countries like Denmark and Israel show this works—their students often outperform peers who went straight to university.

Skeptic: But who can afford a year without income or education? This could widen inequality—wealthy students travel while others fall behind. And what about momentum? Many students who delay never return.

Analyst: The data is mixed. A 2015 study showed gap year students had 0.1-0.4 higher GPAs, but selection bias is significant—motivated students choose gap years. Only 3% of American students take them compared to 25% in the UK.

Creative: What if we reimagined the gap year entirely? Instead of a break, it could be a structured "first year" of hands-on learning—apprenticeships, service, exploration—that counts toward the degree.

Pragmatist: Making it mandatory seems unworkable. Better approach: make gap years easier and more supported. Deferred admission, structured programs, financial aid that covers gap activities.

Synthesis: The panel found common ground on gap years' potential value but diverged on implementation. The Skeptic raised valid equity concerns that the Pragmatist tried to address. The Creative's hybrid model generated interest. Key insight: the question isn't whether gap years help, but how to make them accessible and productive for all students.


Best Practices

  1. Balance perspectives: Ensure no single viewpoint dominates
  2. Keep contributions focused: 2-4 sentences per turn prevents rambling
  3. Encourage engagement: Agents should reference each other's points
  4. Vary the format: Debate, panel, roundtable each serve different purposes
  5. Let users guide depth: Allow follow-up questions to explore specific angles