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

755 lines
22 KiB
Markdown

# 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
```bash
mkdir sprint-planner && cd sprint-planner
go mod init sprint-planner
go get google.golang.org/adk
```
---
## Data Models
```go
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
```go
// 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
```go
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:
```go
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
```bash
# 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**:
```markdown
# 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