Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions agents/architect/architect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package architect

import (
"context"

"github.com/ATMackay/agent/state"
"github.com/ATMackay/agent/tools"
"google.golang.org/adk/agent"
"google.golang.org/adk/agent/llmagent"
adkmodel "google.golang.org/adk/model"
)

const AgentName = "architect"

// Architect produces technical design documents and implementation task lists from PRDs.
type Architect struct {
agent.Agent
}

// NewArchitect returns an Architect agent.
func NewArchitect(ctx context.Context, cfg *Config, mod adkmodel.LLM) (*Architect, error) {
if err := cfg.Validate(); err != nil {
return nil, err
}

deps := tools.Deps{}

kinds := []tools.Kind{
tools.ReadLocalFile,
tools.WriteFile,
tools.SearchFiles,
}
if cfg.WorkDir != "" {
kinds = append(kinds, tools.ListDir)
}

functionTools, err := tools.GetTools(kinds, &deps)
if err != nil {
return nil, err
}

outputKey := state.StateDesign
if cfg.Task == "tasks" {
outputKey = state.StateTaskList
}

ag, err := llmagent.New(llmagent.Config{
Name: AgentName,
Model: mod,
Description: "Produces technical design documents and implementation task lists from product requirements.",
Instruction: buildInstruction(),
Tools: functionTools,
OutputKey: outputKey,
})
if err != nil {
return nil, err
}

return &Architect{Agent: ag}, nil
}
21 changes: 21 additions & 0 deletions agents/architect/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package architect

import "errors"

// Config is the base config for the architect agent.
type Config struct {
WorkDir string // optional: existing project root for context
PRDPath string // path to the input document (PRD or design doc)
OutputPath string
Task string // "design" or "tasks"
}

func (c Config) Validate() error {
if c.OutputPath == "" {
return errors.New("empty output path supplied")
}
if c.Task != "design" && c.Task != "tasks" {
return errors.New("task must be 'design' or 'tasks'")
}
return nil
}
51 changes: 51 additions & 0 deletions agents/architect/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package architect

import "testing"

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg Config
wantErr bool
}{
{
name: "missing output path",
cfg: Config{Task: "design"},
wantErr: true,
},
{
name: "missing task",
cfg: Config{OutputPath: "docs/DESIGN.md"},
wantErr: true,
},
{
name: "invalid task value",
cfg: Config{OutputPath: "docs/DESIGN.md", Task: "review"},
wantErr: true,
},
{
name: "valid design task",
cfg: Config{OutputPath: "docs/DESIGN.md", Task: "design"},
wantErr: false,
},
{
name: "valid tasks task",
cfg: Config{OutputPath: "docs/TASKS.md", Task: "tasks"},
wantErr: false,
},
{
name: "valid with all fields",
cfg: Config{WorkDir: "/src/app", PRDPath: "docs/PRD.md", OutputPath: "docs/DESIGN.md", Task: "design"},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
117 changes: 117 additions & 0 deletions agents/architect/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package architect

import "google.golang.org/genai"

func buildInstruction() string {
return `
You are a software architect. You produce precise, implementable technical specifications
from product requirements documents.

Output path: {output_path}
Input document: {prd_path}
Existing project directory (optional): {work_dir?}
Task: {agent_task}

Workflow:
1. If {prd_path} is set, read it with read_local_file to understand the requirements.
2. If {work_dir} is set, use list_dir and search_files to understand the existing codebase —
read key files (main entry points, core packages, config structures, existing interfaces).
Incorporate existing conventions and avoid proposing changes that conflict with them.
3. Produce the output document for {agent_task} as described below.
4. Write the result with write_output_file.

━━━ When agent_task = "design" ━━━

Produce a Technical Design Document:

# Technical Design — <project name>

## 1. Architecture Overview
Describe the high-level architecture. Include a text diagram (ASCII or Mermaid) of the
main components and their relationships.

## 2. Components & Responsibilities
For each component or package, describe:
- Purpose
- Key responsibilities
- Public interface (types, functions, or API endpoints it exposes)
- Dependencies on other components

## 3. Data Models
Define all significant data structures, schemas, or domain types.
Use pseudocode or the target language syntax.

## 4. API Contracts
For each public API or inter-component interface, specify:
- Method/endpoint signature
- Request/response types
- Error conditions
- Example usage

## 5. Technology Choices
List the key libraries, frameworks, and infrastructure components chosen.
For each, give a one-line rationale.

## 6. Security Considerations
Identify the top 3–5 security concerns and how each is addressed.

## 7. Observability
Describe logging, metrics, and tracing strategy.

## 8. Open Questions
List any design decisions still unresolved.

━━━ When agent_task = "tasks" ━━━

Produce an Implementation Task List from the design document:

# Implementation Task List — <project name>

For each task, use this format:

## Task N: <short title>
**Files:** list files to create or modify
**Description:** what to implement, precisely
**Acceptance Criteria:**
- [ ] criterion 1
- [ ] criterion 2
**Test Requirements:** describe the unit/integration tests that must pass

Rules for the task list:
- Order tasks so dependencies come first (foundational types before business logic,
storage layer before HTTP layer, etc.).
- Keep each task small enough to complete in one focused session (~1–3 hours).
- Every task must have at least one test requirement.
- Use TDD framing: describe what tests prove the task is done.
- Do not include tasks for things already present in the existing codebase.

Efficiency rules (apply to both tasks):
- search_files before reading any file in full.
- Prefer snippet reads (start_line/end_line) over full-file reads.
- Do not read files you have already read unless their content has changed.
- Stop reading when you have enough information to write the output.
`
}

// UserMessage returns the initial message to kick off an architect session.
func UserMessage(inputDocPath, workDir, task string) *genai.Content {
var text string
switch task {
case "tasks":
text = "Read the design document and produce a detailed, ordered implementation task list. " +
"Order tasks so that foundational work comes first. Each task must include test requirements."
default:
text = "Read the PRD and produce a technical design document. " +
"If a work_dir is set, explore it first to understand existing patterns and constraints."
}
if inputDocPath != "" {
text += "\n\nInput document: " + inputDocPath
}
if workDir != "" {
text += "\nExisting project: " + workDir
}
return &genai.Content{
Role: "user",
Parts: []*genai.Part{{Text: text}},
}
}
16 changes: 16 additions & 0 deletions agents/requirements/config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package requirements

import "errors"

// Config is the base config for the requirements analyzer agent.
type Config struct {
WorkDir string // optional: existing project root for context
OutputPath string
}

func (c Config) Validate() error {
if c.OutputPath == "" {
return errors.New("empty output path supplied")
}
return nil
}
36 changes: 36 additions & 0 deletions agents/requirements/config_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package requirements

import "testing"

func TestConfig_Validate(t *testing.T) {
tests := []struct {
name string
cfg Config
wantErr bool
}{
{
name: "missing output path",
cfg: Config{},
wantErr: true,
},
{
name: "valid without work dir",
cfg: Config{OutputPath: "docs/PRD.md"},
wantErr: false,
},
{
name: "valid with work dir",
cfg: Config{WorkDir: "/src/myapp", OutputPath: "docs/PRD.md"},
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
err := tt.cfg.Validate()
if (err != nil) != tt.wantErr {
t.Errorf("Validate() error = %v, wantErr %v", err, tt.wantErr)
}
})
}
}
84 changes: 84 additions & 0 deletions agents/requirements/prompt.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package requirements

import "google.golang.org/genai"

func buildInstruction() string {
return `
You are a software requirements analyst. Your job is to transform raw, informal requirements
into a structured, unambiguous Product Requirements Document (PRD).

Output path: {output_path}
Requirements input: {requirements_prompt}
Existing project directory (optional): {work_dir?}

If a working directory is provided:
1. Call list_dir on {work_dir} to understand the project's current state.
2. Call search_files or read_local_file to read key files such as README, existing docs,
configuration files, or entry points — enough to understand existing tech stack,
conventions, and constraints.
3. Incorporate relevant constraints and existing decisions into the PRD.
If no working directory is provided, skip the exploration steps.

PRD Structure (write all sections):

# Product Requirements Document — <project name>

## 1. Problem Statement
What problem does this solve? Who has this problem? What is the current pain?

## 2. Goals
What must this solution achieve? 3–5 concrete, measurable goals.

## 3. Non-Goals
What is explicitly out of scope? List at least 3 things this project will NOT do.

## 4. Target Users
Who will use this? Describe 1–3 user personas with name, role, and key needs.

## 5. Functional Requirements
List every required capability as a user story:
As a <persona>, I want to <action> so that <outcome>.
For each user story, add numbered acceptance criteria (Given/When/Then).

## 6. Non-Functional Requirements
Address each category if relevant:
- Performance: response time, throughput, concurrency targets
- Security: authentication, authorisation, data protection
- Reliability: uptime, error handling, recovery
- Scalability: growth expectations
- Observability: logging, metrics, tracing
- Developer experience: build time, test coverage targets

## 7. Out of Scope
Explicitly list features, integrations, or concerns this version does not address.

## 8. Success Metrics
How will we know this is a success? List 3–5 measurable criteria.

## 9. Open Questions
List any ambiguities or decisions that still need to be resolved before implementation.

Quality rules:
- Be specific and concrete. Avoid vague terms like "fast", "easy", "better".
- Every functional requirement must have at least one acceptance criterion.
- Do not invent requirements not implied by the input.
- If the input is ambiguous, document the ambiguity in Open Questions rather than guessing.
- Write for a technical audience who will implement directly from this document.

After writing the PRD, call write_output_file with the full document and the output_path.
`
}

// UserMessage returns the initial message to kick off requirements analysis.
func UserMessage(prompt string) *genai.Content {
return &genai.Content{
Role: "user",
Parts: []*genai.Part{
{
Text: "Analyse the following requirements and produce a structured PRD. " +
"If a work_dir is set, explore it first for context.\n\n" +
"Requirements:\n" + prompt,
},
},
}
}
Loading
Loading