stemedb/docs/references/go-adk/agent-planning-facilitator.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

22 KiB

Building a Sprint Planning Meeting Facilitator with ADK-Go

This guide shows how to create a multi-agent system that facilitates project planning and sprint planning meetings, helping teams break down work, estimate effort, and create actionable plans.


Overview

The sprint planning facilitator uses specialized agents for each phase:

┌──────────────────────────────────────────────────────────────────────┐
│                        SPRINT PLANNING FLOW                          │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. INTAKE        2. BREAKDOWN       3. ESTIMATION    4. PLANNING   │
│  ┌──────────┐    ┌──────────┐       ┌──────────┐    ┌──────────┐   │
│  │ Project  │───▶│  Story   │──────▶│Estimator │───▶│ Sprint   │   │
│  │ Analyzer │    │ Creator  │       │          │    │ Planner  │   │
│  └──────────┘    └──────────┘       └──────────┘    └──────────┘   │
│       │               │                  │               │          │
│  Understands     Creates user       Estimates        Organizes     │
│  goals &         stories &          points &         into sprints  │
│  requirements    subtasks           identifies       with goals    │
│                                     risks                          │
└──────────────────────────────────────────────────────────────────────┘

Project Setup

mkdir sprint-planner && cd sprint-planner
go mod init sprint-planner
go get google.golang.org/adk

Data Models

package main

import (
	"context"
	"encoding/json"
	"fmt"
	"log"
	"os"
	"time"

	"google.golang.org/adk/agent"
	"google.golang.org/adk/agent/llmagent"
	"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/genai"
)

// Data structures for planning artifacts

type UserStory struct {
	ID          string   `json:"id"`
	Title       string   `json:"title"`
	Description string   `json:"description"`
	AcceptCrit  []string `json:"acceptance_criteria"`
	Priority    string   `json:"priority"` // must-have, should-have, nice-to-have
	StoryPoints int      `json:"story_points,omitempty"`
	Subtasks    []Task   `json:"subtasks,omitempty"`
	Risks       []string `json:"risks,omitempty"`
	Sprint      int      `json:"sprint,omitempty"`
}

type Task struct {
	ID          string `json:"id"`
	Title       string `json:"title"`
	Description string `json:"description"`
	Hours       int    `json:"estimated_hours"`
	Type        string `json:"type"` // frontend, backend, design, testing, devops
	Blocked     bool   `json:"blocked,omitempty"`
	BlockedBy   string `json:"blocked_by,omitempty"`
}

type Sprint struct {
	Number     int         `json:"number"`
	Goal       string      `json:"goal"`
	StartDate  string      `json:"start_date"`
	EndDate    string      `json:"end_date"`
	Stories    []UserStory `json:"stories"`
	TotalPoints int        `json:"total_points"`
	Capacity   int         `json:"capacity"`
}

type ProjectPlan struct {
	Name        string      `json:"name"`
	Description string      `json:"description"`
	Goals       []string    `json:"goals"`
	Stories     []UserStory `json:"stories"`
	Sprints     []Sprint    `json:"sprints"`
	Risks       []Risk      `json:"risks"`
	Timeline    string      `json:"timeline"`
}

type Risk struct {
	Description string `json:"description"`
	Impact      string `json:"impact"`      // high, medium, low
	Likelihood  string `json:"likelihood"`  // high, medium, low
	Mitigation  string `json:"mitigation"`
}

Planning Tools

// Tool: Create User Story
type CreateStoryInput struct {
	Title       string   `json:"title" jsonschema:"Clear, concise story title"`
	Description string   `json:"description" jsonschema:"As a [user], I want [feature] so that [benefit]"`
	AcceptCrit  []string `json:"acceptance_criteria" jsonschema:"List of acceptance criteria"`
	Priority    string   `json:"priority" jsonschema:"Priority: must-have, should-have, nice-to-have"`
}

type CreateStoryOutput struct {
	StoryID string `json:"story_id"`
	Message string `json:"message"`
}

var storyCounter int
var stories []UserStory

func createStory(ctx tool.Context, input CreateStoryInput) CreateStoryOutput {
	storyCounter++
	id := fmt.Sprintf("US-%03d", storyCounter)

	story := UserStory{
		ID:          id,
		Title:       input.Title,
		Description: input.Description,
		AcceptCrit:  input.AcceptCrit,
		Priority:    input.Priority,
	}
	stories = append(stories, story)

	// Save to state
	ctx.Session().State().Set("stories", stories)

	return CreateStoryOutput{
		StoryID: id,
		Message: fmt.Sprintf("Created story %s: %s", id, input.Title),
	}
}

// Tool: Add Subtask to Story
type AddSubtaskInput struct {
	StoryID     string `json:"story_id" jsonschema:"Parent story ID (e.g., US-001)"`
	Title       string `json:"title" jsonschema:"Task title"`
	Description string `json:"description" jsonschema:"Task description"`
	Hours       int    `json:"estimated_hours" jsonschema:"Estimated hours to complete"`
	Type        string `json:"type" jsonschema:"Type: frontend, backend, design, testing, devops, documentation"`
}

type AddSubtaskOutput struct {
	TaskID  string `json:"task_id"`
	Message string `json:"message"`
}

var taskCounter int

func addSubtask(ctx tool.Context, input AddSubtaskInput) AddSubtaskOutput {
	taskCounter++
	taskID := fmt.Sprintf("T-%03d", taskCounter)

	task := Task{
		ID:          taskID,
		Title:       input.Title,
		Description: input.Description,
		Hours:       input.Hours,
		Type:        input.Type,
	}

	// Find and update story
	for i := range stories {
		if stories[i].ID == input.StoryID {
			stories[i].Subtasks = append(stories[i].Subtasks, task)
			ctx.Session().State().Set("stories", stories)
			return AddSubtaskOutput{
				TaskID:  taskID,
				Message: fmt.Sprintf("Added task %s to %s", taskID, input.StoryID),
			}
		}
	}

	return AddSubtaskOutput{
		TaskID:  "",
		Message: fmt.Sprintf("Story %s not found", input.StoryID),
	}
}

// Tool: Estimate Story Points
type EstimateStoryInput struct {
	StoryID     string `json:"story_id" jsonschema:"Story ID to estimate"`
	Points      int    `json:"points" jsonschema:"Story points (1, 2, 3, 5, 8, 13)"`
	Rationale   string `json:"rationale" jsonschema:"Brief explanation for estimate"`
}

type EstimateStoryOutput struct {
	Message string `json:"message"`
}

func estimateStory(ctx tool.Context, input EstimateStoryInput) EstimateStoryOutput {
	for i := range stories {
		if stories[i].ID == input.StoryID {
			stories[i].StoryPoints = input.Points
			ctx.Session().State().Set("stories", stories)
			return EstimateStoryOutput{
				Message: fmt.Sprintf("Estimated %s at %d points: %s", input.StoryID, input.Points, input.Rationale),
			}
		}
	}
	return EstimateStoryOutput{Message: "Story not found"}
}

// Tool: Add Risk
type AddRiskInput struct {
	Description string `json:"description" jsonschema:"Risk description"`
	Impact      string `json:"impact" jsonschema:"Impact level: high, medium, low"`
	Likelihood  string `json:"likelihood" jsonschema:"Likelihood: high, medium, low"`
	Mitigation  string `json:"mitigation" jsonschema:"Mitigation strategy"`
	StoryID     string `json:"story_id,omitempty" jsonschema:"Related story ID (optional)"`
}

type AddRiskOutput struct {
	Message string `json:"message"`
}

var risks []Risk

func addRisk(ctx tool.Context, input AddRiskInput) AddRiskOutput {
	risk := Risk{
		Description: input.Description,
		Impact:      input.Impact,
		Likelihood:  input.Likelihood,
		Mitigation:  input.Mitigation,
	}
	risks = append(risks, risk)

	// Also add to story if specified
	if input.StoryID != "" {
		for i := range stories {
			if stories[i].ID == input.StoryID {
				stories[i].Risks = append(stories[i].Risks, input.Description)
			}
		}
	}

	ctx.Session().State().Set("risks", risks)
	ctx.Session().State().Set("stories", stories)

	return AddRiskOutput{Message: fmt.Sprintf("Added risk: %s", input.Description)}
}

// Tool: Assign to Sprint
type AssignSprintInput struct {
	StoryID  string `json:"story_id" jsonschema:"Story ID to assign"`
	SprintNum int   `json:"sprint_number" jsonschema:"Sprint number (1, 2, 3, etc.)"`
}

type AssignSprintOutput struct {
	Message string `json:"message"`
}

func assignSprint(ctx tool.Context, input AssignSprintInput) AssignSprintOutput {
	for i := range stories {
		if stories[i].ID == input.StoryID {
			stories[i].Sprint = input.SprintNum
			ctx.Session().State().Set("stories", stories)
			return AssignSprintOutput{
				Message: fmt.Sprintf("Assigned %s to Sprint %d", input.StoryID, input.SprintNum),
			}
		}
	}
	return AssignSprintOutput{Message: "Story not found"}
}

// Tool: Generate Plan Summary
type GeneratePlanInput struct {
	SprintLength int `json:"sprint_length_days" jsonschema:"Sprint length in days (default: 14)"`
	Velocity     int `json:"velocity" jsonschema:"Team velocity (story points per sprint)"`
}

type GeneratePlanOutput struct {
	Summary string          `json:"summary"`
	Plan    json.RawMessage `json:"plan"`
}

func generatePlan(ctx tool.Context, input GeneratePlanInput) GeneratePlanOutput {
	sprintLength := input.SprintLength
	if sprintLength == 0 {
		sprintLength = 14
	}

	// Group stories by sprint
	sprintMap := make(map[int][]UserStory)
	totalPoints := 0
	for _, s := range stories {
		sprintMap[s.Sprint] = append(sprintMap[s.Sprint], s)
		totalPoints += s.StoryPoints
	}

	// Build sprints
	var sprints []Sprint
	startDate := time.Now()
	for num := 1; num <= len(sprintMap); num++ {
		sprintStories := sprintMap[num]
		points := 0
		for _, s := range sprintStories {
			points += s.StoryPoints
		}

		sprint := Sprint{
			Number:      num,
			StartDate:   startDate.Format("2006-01-02"),
			EndDate:     startDate.AddDate(0, 0, sprintLength).Format("2006-01-02"),
			Stories:     sprintStories,
			TotalPoints: points,
			Capacity:    input.Velocity,
		}
		sprints = append(sprints, sprint)
		startDate = startDate.AddDate(0, 0, sprintLength)
	}

	plan := ProjectPlan{
		Stories: stories,
		Sprints: sprints,
		Risks:   risks,
	}

	planJSON, _ := json.MarshalIndent(plan, "", "  ")

	summary := fmt.Sprintf(
		"Plan: %d stories, %d total points, %d sprints, %d risks identified",
		len(stories), totalPoints, len(sprints), len(risks),
	)

	return GeneratePlanOutput{
		Summary: summary,
		Plan:    planJSON,
	}
}

Building the Planning Pipeline

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)
	}

	planner, err := buildPlanningPipeline(model)
	if err != nil {
		log.Fatal(err)
	}

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

func buildPlanningPipeline(model *gemini.Model) (agent.Agent, error) {
	tools, err := createPlanningTools()
	if err != nil {
		return nil, err
	}

	// Stage 1: Project Analyzer
	analyzer, err := llmagent.New(llmagent.Config{
		Name:        "project_analyzer",
		Model:       model,
		Description: "Analyzes project requirements and extracts goals",
		Instruction: `You are a senior product manager analyzing a project.

Given the project description, identify:
1. **Core Goals**: What are the main objectives? (3-5 goals)
2. **User Types**: Who are the users/stakeholders?
3. **Key Features**: What are the must-have features?
4. **Constraints**: Timeline, technical, or resource constraints mentioned
5. **Success Metrics**: How will success be measured?

Be thorough but concise. Output a structured analysis.`,
		OutputKey: "project_analysis",
	})
	if err != nil {
		return nil, err
	}

	// Stage 2: Story Creator
	storyCreator, err := llmagent.New(llmagent.Config{
		Name:        "story_creator",
		Model:       model,
		Description: "Creates user stories from requirements",
		Instruction: `You are an agile coach creating user stories.

PROJECT ANALYSIS:
{project_analysis}

Create user stories for each feature identified. For each story:
1. Use the create_story tool with proper format
2. Write clear acceptance criteria (testable conditions)
3. Assign appropriate priority (must-have, should-have, nice-to-have)
4. Use the add_subtask tool to break down into technical tasks

Story format: "As a [user type], I want [feature] so that [benefit]"

Create stories for ALL identified features. Be comprehensive.
Break down large features into multiple smaller stories.`,
		Tools:     tools,
		OutputKey: "stories_created",
	})
	if err != nil {
		return nil, err
	}

	// Stage 3: Estimator
	estimator, err := llmagent.New(llmagent.Config{
		Name:        "estimator",
		Model:       model,
		Description: "Estimates story points and identifies risks",
		Instruction: `You are a tech lead estimating work and identifying risks.

STORIES CREATED:
{stories_created}

For each story:
1. Use estimate_story to assign story points (Fibonacci: 1, 2, 3, 5, 8, 13)
   - 1-2: Simple, well-understood
   - 3-5: Medium complexity
   - 8: Complex, some unknowns
   - 13: Very complex, needs breakdown

2. Use add_risk for any risks you identify:
   - Technical risks (new technology, integration challenges)
   - Dependency risks (external teams, third-party services)
   - Scope risks (unclear requirements, likely changes)

Consider:
- Task complexity from subtasks
- Dependencies between stories
- Team's likely familiarity with the tech
- Uncertainty and unknowns`,
		Tools:     tools,
		OutputKey: "estimates_complete",
	})
	if err != nil {
		return nil, err
	}

	// Stage 4: Sprint Planner
	sprintPlanner, err := llmagent.New(llmagent.Config{
		Name:        "sprint_planner",
		Model:       model,
		Description: "Organizes stories into sprints",
		Instruction: `You are a scrum master organizing the sprint plan.

ESTIMATES:
{estimates_complete}

Team capacity: Assume 30-40 story points per 2-week sprint.

Organize stories into sprints:
1. Use assign_sprint to place each story in a sprint
2. Prioritize must-have stories in early sprints
3. Consider dependencies (blocked stories go later)
4. Balance sprint workloads (don't exceed capacity)
5. Create logical sprint goals (each sprint delivers value)

After assigning all stories, use generate_plan to create the final summary.

Sprint planning principles:
- Sprint 1: Foundation and core features
- Middle sprints: Build on foundation
- Final sprint: Polish, testing, nice-to-haves`,
		Tools:     tools,
		OutputKey: "sprint_plan",
	})
	if err != nil {
		return nil, err
	}

	// Stage 5: Plan Presenter
	presenter, err := llmagent.New(llmagent.Config{
		Name:        "presenter",
		Model:       model,
		Description: "Presents the final plan in readable format",
		Instruction: `Present the sprint plan in a clear, actionable format.

SPRINT PLAN:
{sprint_plan}

Create a presentation that includes:

## Executive Summary
- Total scope and timeline
- Key risks and mitigations

## Sprint Breakdown
For each sprint:
- Sprint goal (one sentence)
- Stories included with points
- Key deliverables

## Risk Register
- High priority risks with mitigation plans

## Recommendations
- Any suggestions for the team
- Dependencies to resolve early
- Decisions needed

Format for easy reading and sharing with stakeholders.`,
		OutputKey: "final_presentation",
	})
	if err != nil {
		return nil, err
	}

	// Assemble pipeline
	return sequentialagent.New(sequentialagent.Config{
		AgentConfig: agent.Config{
			Name:        "sprint_planning_facilitator",
			Description: "Facilitates complete sprint planning from requirements to actionable plan",
			SubAgents:   []agent.Agent{analyzer, storyCreator, estimator, sprintPlanner, presenter},
		},
	})
}

func createPlanningTools() ([]tool.Tool, error) {
	var tools []tool.Tool

	createStoryTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "create_story",
			Description: "Create a new user story",
		},
		createStory,
	)
	tools = append(tools, createStoryTool)

	addSubtaskTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "add_subtask",
			Description: "Add a technical subtask to a user story",
		},
		addSubtask,
	)
	tools = append(tools, addSubtaskTool)

	estimateTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "estimate_story",
			Description: "Assign story points to a user story",
		},
		estimateStory,
	)
	tools = append(tools, estimateTool)

	riskTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "add_risk",
			Description: "Document a project risk with mitigation",
		},
		addRisk,
	)
	tools = append(tools, riskTool)

	sprintTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "assign_sprint",
			Description: "Assign a story to a specific sprint",
		},
		assignSprint,
	)
	tools = append(tools, sprintTool)

	planTool, _ := functiontool.New(
		functiontool.Config{
			Name:        "generate_plan",
			Description: "Generate the final sprint plan summary",
		},
		generatePlan,
	)
	tools = append(tools, planTool)

	return tools, nil
}

Interactive Planning Mode

For collaborative sessions where the user provides input at each stage:

func buildInteractivePlanner(model *gemini.Model) (agent.Agent, error) {
	tools, _ := createPlanningTools()

	return llmagent.New(llmagent.Config{
		Name:  "interactive_planner",
		Model: model,
		Instruction: `You are an agile coach facilitating a sprint planning session.

CURRENT STATE:
Stories: {stories}
Risks: {risks}

Guide the user through planning:

1. **If no stories exist**: Ask about the project and help create stories
2. **If stories need breakdown**: Help add subtasks
3. **If stories need estimates**: Facilitate estimation discussion
4. **If stories need sprint assignment**: Help prioritize and assign
5. **If plan is complete**: Offer to present or refine

Available commands the user might give:
- "Let's plan [project description]" → Start fresh
- "Add a story for [feature]" → Create specific story
- "Break down [story ID]" → Add subtasks
- "Estimate [story ID]" → Discuss and set points
- "Show the plan" → Generate current plan
- "What's in sprint [N]?" → Show sprint details

Be collaborative. Ask clarifying questions. Suggest improvements.
Use tools to track everything properly.`,
		Tools: tools,
	})
}

Running the Planner

# Run sprint planning
go run main.go

# Example prompts:
# > Plan a mobile app for food delivery with user ordering, restaurant management, and driver tracking
# > We're building an internal dashboard for sales analytics with charts, reports, and alerts
# > Create a sprint plan for migrating our monolith to microservices

# Web UI for visual debugging
go run main.go web api webui

Sample Output

User: Plan a task management app with projects, tasks, due dates, and team collaboration

Output:

# Sprint Plan: Task Management App

## Executive Summary

- **Scope**: 12 user stories, 47 story points
- **Timeline**: 3 sprints (6 weeks)
- **Key Risks**: Real-time sync complexity, notification deliverability

## Sprint 1: Foundation (Weeks 1-2)

**Goal**: Core task and project management

| ID     | Story                                | Points |
| ------ | ------------------------------------ | ------ |
| US-001 | User registration and authentication | 5      |
| US-002 | Create and manage projects           | 3      |
| US-003 | Create, edit, delete tasks           | 5      |
| US-004 | Set due dates with calendar picker   | 3      |

**Total**: 16 points | **Deliverable**: Users can create projects and tasks

## Sprint 2: Collaboration (Weeks 3-4)

**Goal**: Team features and organization

| ID     | Story                           | Points |
| ------ | ------------------------------- | ------ |
| US-005 | Invite team members to projects | 5      |
| US-006 | Assign tasks to team members    | 3      |
| US-007 | Task comments and activity feed | 5      |
| US-008 | Task labels and filtering       | 3      |

**Total**: 16 points | **Deliverable**: Teams can collaborate on projects

## Sprint 3: Polish (Weeks 5-6)

**Goal**: Notifications and UX improvements

| ID     | Story                             | Points |
| ------ | --------------------------------- | ------ |
| US-009 | Email notifications for due dates | 5      |
| US-010 | Push notifications (mobile)       | 5      |
| US-011 | Dashboard with task overview      | 3      |
| US-012 | Search across all tasks           | 2      |

**Total**: 15 points | **Deliverable**: Complete MVP ready for beta

## Risk Register

| Risk                         | Impact | Likelihood | Mitigation                                                      |
| ---------------------------- | ------ | ---------- | --------------------------------------------------------------- |
| Real-time sync conflicts     | High   | Medium     | Implement optimistic locking, conflict resolution UI            |
| Push notification delivery   | Medium | Medium     | Use established service (Firebase), implement fallback to email |
| Scope creep on collaboration | Medium | High       | Strict MVP definition, defer advanced features                  |

## Recommendations

1. **Decide early**: Authentication provider (build vs. Auth0/Firebase)
2. **Spike needed**: Real-time sync approach (WebSockets vs. polling)
3. **Dependency**: Mobile notification setup requires Apple/Google developer accounts

Best Practices

  1. Start with clear requirements: The better the input, the better the plan
  2. Review intermediate outputs: Check stories before estimation
  3. Adjust velocity: Set realistic team capacity based on actual data
  4. Iterate: Use interactive mode to refine the plan collaboratively
  5. Export and track: Copy the plan to your actual project management tool
  6. Re-plan as needed: Run again when scope changes significantly