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