# 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