From be841ea3421ee55c66ff1db37996537f8ddd12c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 11:17:28 +0000 Subject: [PATCH 01/19] feat: add micro new --prompt and micro run --prompt MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add AI-powered service generation: describe a system in natural language and get real go-micro services with proto definitions, handlers, doc comments, and MCP support. micro new --prompt "a contact book with notes and tags" \ --provider anthropic Generates: contacts/ — CRUD service with name, email, phone fields notes/ — notes linked to contacts tags/ — tagging system Each service gets: proto/{name}.proto — domain model + CRUD endpoints handler/{name}.go — in-memory store, @example tags for MCP main.go — MCP-enabled, proper imports go.mod + Makefile — compiles with go mod tidy + make proto micro run --prompt does the same then starts all services. The LLM designs the architecture (service names, fields, endpoints, descriptions) and returns structured JSON. Code generation uses the existing template patterns — the output is standard go-micro code that compiles, runs, and is immediately callable via MCP and micro chat. No AI dependency at runtime. --- cmd/micro/cli/cli.go | 15 + cmd/micro/cli/generate/generate.go | 451 +++++++++++++++++++++++++++++ cmd/micro/cli/new/new.go | 63 ++++ cmd/micro/run/run.go | 72 +++++ 4 files changed, 601 insertions(+) create mode 100644 cmd/micro/cli/generate/generate.go diff --git a/cmd/micro/cli/cli.go b/cmd/micro/cli/cli.go index 024f4995c9..7d62be2c2b 100644 --- a/cmd/micro/cli/cli.go +++ b/cmd/micro/cli/cli.go @@ -52,6 +52,21 @@ func init() { Name: "template", Usage: "Service template: default, crud, pubsub, api", }, + &cli.StringFlag{ + Name: "prompt", + Usage: "Describe the system to generate (uses AI to design services)", + EnvVars: []string{"MICRO_NEW_PROMPT"}, + }, + &cli.StringFlag{ + Name: "provider", + Usage: "AI provider for --prompt (anthropic, openai, gemini, etc.)", + EnvVars: []string{"MICRO_AI_PROVIDER"}, + }, + &cli.StringFlag{ + Name: "api_key", + Usage: "API key for --prompt", + EnvVars: []string{"MICRO_AI_API_KEY"}, + }, }, }, { diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go new file mode 100644 index 0000000000..89d1dd67e6 --- /dev/null +++ b/cmd/micro/cli/generate/generate.go @@ -0,0 +1,451 @@ +// Package generate implements 'micro new --prompt' and 'micro run --prompt' +// which use an LLM to design services from a natural language description, +// then generate real go-micro code using the existing template system. +package generate + +import ( + "context" + "encoding/json" + "fmt" + "os" + "os/exec" + "path/filepath" + "strings" + + "go-micro.dev/v5/ai" + + // Register providers so ai.New works. + _ "go-micro.dev/v5/ai/anthropic" + _ "go-micro.dev/v5/ai/atlascloud" + _ "go-micro.dev/v5/ai/gemini" + _ "go-micro.dev/v5/ai/groq" + _ "go-micro.dev/v5/ai/mistral" + _ "go-micro.dev/v5/ai/openai" + _ "go-micro.dev/v5/ai/together" +) + +const designPrompt = `You are a Go microservices architect. Given a description of a system, design the services needed. + +Return ONLY valid JSON with this exact structure: +{ + "services": [ + { + "name": "service-name", + "description": "What this service does", + "fields": [ + {"name": "field_name", "type": "string", "description": "What this field is"} + ], + "endpoints": [ + {"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"} + ] + } + ] +} + +Rules: +- Service names are lowercase, hyphenated (e.g. "order-items") +- Each service should have CRUD endpoints (Create, Read, Update, Delete, List) plus any custom ones +- Field types must be: string, int64, bool, float64 +- Every service must have an "id" field (string) and "created"/"updated" fields (int64) +- Endpoint names are PascalCase (e.g. "Create", "FindByEmail") +- The example field shows a realistic JSON input for the endpoint +- Keep it focused — 2-5 services max +- Each endpoint description should be clear enough for an AI agent to understand when to call it` + +// ServiceDesign is the LLM's output describing the system architecture. +type ServiceDesign struct { + Services []ServiceSpec `json:"services"` +} + +// ServiceSpec describes one service to generate. +type ServiceSpec struct { + Name string `json:"name"` + Description string `json:"description"` + Fields []FieldSpec `json:"fields"` + Endpoints []EndpointSpec `json:"endpoints"` +} + +// FieldSpec describes a field on the service's record type. +type FieldSpec struct { + Name string `json:"name"` + Type string `json:"type"` + Description string `json:"description"` +} + +// EndpointSpec describes an RPC endpoint. +type EndpointSpec struct { + Name string `json:"name"` + Description string `json:"description"` + Example string `json:"example"` +} + +// Design calls an LLM to design services from a natural language prompt. +func Design(ctx context.Context, provider, apiKey, model, prompt string) (*ServiceDesign, error) { + if provider == "" { + provider = ai.AutoDetectProvider("") + } + + var opts []ai.Option + opts = append(opts, ai.WithAPIKey(apiKey)) + if model != "" { + opts = append(opts, ai.WithModel(model)) + } + + m := ai.New(provider, opts...) + if m == nil { + return nil, fmt.Errorf("unknown provider: %s", provider) + } + + resp, err := m.Generate(ctx, &ai.Request{ + Prompt: fmt.Sprintf("Design a microservices system for: %s", prompt), + SystemPrompt: designPrompt, + }) + if err != nil { + return nil, fmt.Errorf("LLM design failed: %w", err) + } + + reply := resp.Reply + if resp.Answer != "" { + reply = resp.Answer + } + + // Extract JSON from the response (LLM may wrap it in markdown) + reply = extractJSON(reply) + + var design ServiceDesign + if err := json.Unmarshal([]byte(reply), &design); err != nil { + return nil, fmt.Errorf("failed to parse LLM response as JSON: %w\nResponse: %s", err, reply) + } + + if len(design.Services) == 0 { + return nil, fmt.Errorf("LLM returned no services") + } + + return &design, nil +} + +// Generate creates go-micro service directories from a design. +// Each service gets proto, handler, main.go, Makefile, go.mod. +func Generate(baseDir string, design *ServiceDesign) error { + for _, svc := range design.Services { + svcDir := filepath.Join(baseDir, svc.Name) + if err := generateService(svcDir, svc); err != nil { + return fmt.Errorf("generate %s: %w", svc.Name, err) + } + } + return nil +} + +func generateService(dir string, svc ServiceSpec) error { + if err := os.MkdirAll(filepath.Join(dir, "handler"), 0755); err != nil { + return err + } + if err := os.MkdirAll(filepath.Join(dir, "proto"), 0755); err != nil { + return err + } + + name := svc.Name + titleName := toTitle(name) + + // Proto + proto := generateProto(name, titleName, svc) + if err := os.WriteFile(filepath.Join(dir, "proto", name+".proto"), []byte(proto), 0644); err != nil { + return err + } + + // Handler + handler := generateHandler(name, titleName, svc) + if err := os.WriteFile(filepath.Join(dir, "handler", name+".go"), []byte(handler), 0644); err != nil { + return err + } + + // Main + main := generateMain(name, titleName) + if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(main), 0644); err != nil { + return err + } + + // Makefile + makefile := fmt.Sprintf("GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") + if err := os.WriteFile(filepath.Join(dir, "Makefile"), []byte(makefile), 0644); err != nil { + return err + } + + // go.mod + gomod := fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name) + if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil { + return err + } + + // Run go mod tidy and make proto + runIn(dir, "go", "mod", "tidy") + runIn(dir, "make", "proto") + + return nil +} + +func generateProto(name, titleName string, svc ServiceSpec) string { + dehyphen := strings.ReplaceAll(name, "-", "") + var b strings.Builder + b.WriteString(fmt.Sprintf("syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen)) + + // Service definition + b.WriteString(fmt.Sprintf("service %s {\n", titleName)) + for _, ep := range svc.Endpoints { + reqName := ep.Name + "Request" + rspName := ep.Name + "Response" + if ep.Name == "List" { + reqName = "ListRequest" + rspName = "ListResponse" + } + b.WriteString(fmt.Sprintf("\trpc %s(%s) returns (%s) {}\n", ep.Name, reqName, rspName)) + } + b.WriteString("}\n\n") + + // Record message + b.WriteString(fmt.Sprintf("message %sRecord {\n", titleName)) + fieldNum := 1 + for _, f := range svc.Fields { + b.WriteString(fmt.Sprintf("\t// %s\n", f.Description)) + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fieldNum)) + fieldNum++ + } + b.WriteString("}\n\n") + + // Request/response messages for each endpoint + for _, ep := range svc.Endpoints { + switch ep.Name { + case "Create": + b.WriteString(fmt.Sprintf("message CreateRequest {\n")) + fn := 1 + for _, f := range svc.Fields { + if f.Name == "id" || f.Name == "created" || f.Name == "updated" { + continue + } + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) + fn++ + } + b.WriteString("}\n\n") + b.WriteString(fmt.Sprintf("message CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + case "Read": + b.WriteString("message ReadRequest {\n\tstring id = 1;\n}\n\n") + b.WriteString(fmt.Sprintf("message ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + case "Update": + b.WriteString("message UpdateRequest {\n\tstring id = 1;\n") + fn := 2 + for _, f := range svc.Fields { + if f.Name == "id" || f.Name == "created" || f.Name == "updated" { + continue + } + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) + fn++ + } + b.WriteString("}\n\n") + b.WriteString(fmt.Sprintf("message UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + case "Delete": + b.WriteString("message DeleteRequest {\n\tstring id = 1;\n}\n\n") + b.WriteString("message DeleteResponse {\n\tbool deleted = 1;\n}\n\n") + case "List": + b.WriteString("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n}\n\n") + b.WriteString(fmt.Sprintf("message ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName)) + default: + // Custom endpoint — request has the fields, response has a result string + b.WriteString(fmt.Sprintf("message %sRequest {\n", ep.Name)) + fn := 1 + for _, f := range svc.Fields { + if f.Name == "created" || f.Name == "updated" { + continue + } + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) + fn++ + } + b.WriteString("}\n\n") + b.WriteString(fmt.Sprintf("message %sResponse {\n\tstring result = 1;\n}\n\n", ep.Name)) + } + } + + return b.String() +} + +func generateHandler(name, titleName string, svc ServiceSpec) string { + dehyphen := strings.ReplaceAll(name, "-", "") + var b strings.Builder + + b.WriteString("package handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n") + b.WriteString("\t\"github.com/google/uuid\"\n") + b.WriteString(fmt.Sprintf("\tlog \"go-micro.dev/v5/logger\"\n\n\tpb \"%s/proto\"\n)\n\n", name)) + + b.WriteString(fmt.Sprintf("type %s struct {\n\tmu sync.RWMutex\n\trecords map[string]*pb.%sRecord\n}\n\n", titleName, titleName)) + b.WriteString(fmt.Sprintf("func New() *%s {\n\treturn &%s{records: make(map[string]*pb.%sRecord)}\n}\n\n", titleName, titleName, titleName)) + + for _, ep := range svc.Endpoints { + example := ep.Example + if example == "" { + example = "{}" + } + b.WriteString(fmt.Sprintf("// %s %s\n//\n// @example %s\n", ep.Name, ep.Description, example)) + + switch ep.Name { + case "Create": + b.WriteString(fmt.Sprintf("func (h *%s) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {\n", titleName)) + b.WriteString(fmt.Sprintf("\tlog.Infof(\"Creating %s record\")\n", dehyphen)) + b.WriteString("\tnow := time.Now().Unix()\n") + b.WriteString(fmt.Sprintf("\trecord := &pb.%sRecord{\n\t\tId: uuid.New().String(),\n", titleName)) + for _, f := range svc.Fields { + if f.Name == "id" || f.Name == "created" || f.Name == "updated" { + continue + } + b.WriteString(fmt.Sprintf("\t\t%s: req.%s,\n", toTitle(f.Name), toTitle(f.Name))) + } + b.WriteString("\t\tCreated: now,\n\t\tUpdated: now,\n\t}\n") + b.WriteString("\th.mu.Lock()\n\th.records[record.Id] = record\n\th.mu.Unlock()\n\trsp.Record = record\n\treturn nil\n}\n\n") + + case "Read": + b.WriteString(fmt.Sprintf("func (h *%s) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {\n", titleName)) + b.WriteString("\th.mu.RLock()\n\trecord, ok := h.records[req.Id]\n\th.mu.RUnlock()\n") + b.WriteString("\tif !ok {\n\t\treturn fmt.Errorf(\"record %s not found\", req.Id)\n\t}\n") + b.WriteString("\trsp.Record = record\n\treturn nil\n}\n\n") + + case "Update": + b.WriteString(fmt.Sprintf("func (h *%s) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {\n", titleName)) + b.WriteString("\th.mu.Lock()\n\tdefer h.mu.Unlock()\n") + b.WriteString("\trecord, ok := h.records[req.Id]\n") + b.WriteString("\tif !ok {\n\t\treturn fmt.Errorf(\"record %s not found\", req.Id)\n\t}\n") + for _, f := range svc.Fields { + if f.Name == "id" || f.Name == "created" || f.Name == "updated" { + continue + } + switch f.Type { + case "string": + b.WriteString(fmt.Sprintf("\tif req.%s != \"\" {\n\t\trecord.%s = req.%s\n\t}\n", toTitle(f.Name), toTitle(f.Name), toTitle(f.Name))) + case "int64": + b.WriteString(fmt.Sprintf("\tif req.%s != 0 {\n\t\trecord.%s = req.%s\n\t}\n", toTitle(f.Name), toTitle(f.Name), toTitle(f.Name))) + case "bool": + b.WriteString(fmt.Sprintf("\trecord.%s = req.%s\n", toTitle(f.Name), toTitle(f.Name))) + default: + b.WriteString(fmt.Sprintf("\trecord.%s = req.%s\n", toTitle(f.Name), toTitle(f.Name))) + } + } + b.WriteString("\trecord.Updated = time.Now().Unix()\n\trsp.Record = record\n\treturn nil\n}\n\n") + + case "Delete": + b.WriteString(fmt.Sprintf("func (h *%s) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {\n", titleName)) + b.WriteString("\th.mu.Lock()\n\t_, ok := h.records[req.Id]\n\tif ok {\n\t\tdelete(h.records, req.Id)\n\t}\n\th.mu.Unlock()\n") + b.WriteString("\trsp.Deleted = ok\n\treturn nil\n}\n\n") + + case "List": + b.WriteString(fmt.Sprintf("func (h *%s) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {\n", titleName)) + b.WriteString(fmt.Sprintf("\th.mu.RLock()\n\tdefer h.mu.RUnlock()\n\tall := make([]*pb.%sRecord, 0, len(h.records))\n", titleName)) + b.WriteString("\tfor _, r := range h.records {\n\t\tall = append(all, r)\n\t}\n") + b.WriteString("\tsort.Slice(all, func(i, j int) bool { return all[i].Created > all[j].Created })\n") + b.WriteString("\trsp.Total = int64(len(all))\n") + b.WriteString("\toffset := int(req.Offset)\n\tif offset > len(all) { offset = len(all) }\n") + b.WriteString("\tlimit := int(req.Limit)\n\tif limit <= 0 { limit = 20 }\n") + b.WriteString("\tend := offset + limit\n\tif end > len(all) { end = len(all) }\n") + b.WriteString("\trsp.Records = all[offset:end]\n\treturn nil\n}\n\n") + + default: + // Custom endpoint — simple stub + b.WriteString(fmt.Sprintf("func (h *%s) %s(ctx context.Context, req *pb.%sRequest, rsp *pb.%sResponse) error {\n", + titleName, ep.Name, ep.Name, ep.Name)) + b.WriteString(fmt.Sprintf("\tlog.Infof(\"%s.%s called\")\n", titleName, ep.Name)) + b.WriteString("\trsp.Result = \"ok\"\n\treturn nil\n}\n\n") + } + } + + // Suppress unused imports + b.WriteString("var _ = fmt.Sprintf\nvar _ = sort.Slice\n") + + return b.String() +} + +func generateMain(name, titleName string) string { + return fmt.Sprintf(`package main + +import ( + "%s/handler" + pb "%s/proto" + + "go-micro.dev/v5" + "go-micro.dev/v5/gateway/mcp" +) + +func main() { + service := micro.New("%s", + mcp.WithMCP(":3001"), + ) + + service.Init() + + pb.Register%sHandler(service.Server(), handler.New()) + + service.Run() +} +`, name, name, strings.ReplaceAll(name, "-", ""), titleName) +} + +func extractJSON(s string) string { + // Try to find JSON in markdown code blocks + if i := strings.Index(s, "```json"); i >= 0 { + s = s[i+7:] + if j := strings.Index(s, "```"); j >= 0 { + return strings.TrimSpace(s[:j]) + } + } + if i := strings.Index(s, "```"); i >= 0 { + s = s[i+3:] + if j := strings.Index(s, "```"); j >= 0 { + return strings.TrimSpace(s[:j]) + } + } + // Try to find raw JSON + if i := strings.Index(s, "{"); i >= 0 { + depth := 0 + for j := i; j < len(s); j++ { + if s[j] == '{' { + depth++ + } else if s[j] == '}' { + depth-- + if depth == 0 { + return s[i : j+1] + } + } + } + } + return s +} + +func protoType(goType string) string { + switch goType { + case "int64": + return "int64" + case "int32": + return "int32" + case "bool": + return "bool" + case "float64": + return "double" + default: + return "string" + } +} + +func toTitle(s string) string { + s = strings.ReplaceAll(s, "-", " ") + s = strings.ReplaceAll(s, "_", " ") + words := strings.Fields(s) + for i, w := range words { + if len(w) > 0 { + words[i] = strings.ToUpper(w[:1]) + w[1:] + } + } + return strings.Join(words, "") +} + +func runIn(dir string, name string, args ...string) error { + cmd := exec.Command(name, args...) + cmd.Dir = dir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} diff --git a/cmd/micro/cli/new/new.go b/cmd/micro/cli/new/new.go index e5a475a3dd..7fe89d382f 100644 --- a/cmd/micro/cli/new/new.go +++ b/cmd/micro/cli/new/new.go @@ -2,6 +2,7 @@ package new import ( + "context" "fmt" "go/build" "os" @@ -10,6 +11,8 @@ import ( "path/filepath" "runtime" "strings" + + "go-micro.dev/v5/cmd/micro/cli/generate" "text/template" "time" @@ -138,6 +141,11 @@ func addFileToTree(root treeprint.Tree, file string) { } func Run(ctx *cli.Context) error { + // Handle --prompt: design services with AI, then generate each one + if prompt := ctx.String("prompt"); prompt != "" { + return runPrompt(ctx, prompt) + } + dir := ctx.Args().First() if len(dir) == 0 { fmt.Println("specify service name") @@ -307,3 +315,58 @@ func printTree(dir string) { filepath.Walk(dir, walk) fmt.Println(t.String()) } + +func runPrompt(ctx *cli.Context, prompt string) error { + provider := ctx.String("provider") + apiKey := ctx.String("api_key") + if apiKey == "" { + // Try provider-specific env vars + for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", + "ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} { + if v := os.Getenv(env); v != "" { + apiKey = v + break + } + } + } + if apiKey == "" { + return fmt.Errorf("--api_key or a provider API key env var is required for --prompt") + } + + fmt.Println() + fmt.Println(" \033[1mmicro new --prompt\033[0m") + fmt.Println() + fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt) + + design, err := generate.Design(context.Background(), provider, apiKey, "", prompt) + if err != nil { + return fmt.Errorf("design failed: %w", err) + } + + fmt.Println(" Services:") + for _, svc := range design.Services { + fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description) + for _, ep := range svc.Endpoints { + fmt.Printf(" %s: %s\n", ep.Name, ep.Description) + } + } + fmt.Println() + + fmt.Println(" Generating code...") + if err := generate.Generate(".", design); err != nil { + return fmt.Errorf("generate failed: %w", err) + } + + for _, svc := range design.Services { + fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name) + } + fmt.Println() + + fmt.Println(" \033[32m✓\033[0m All services generated") + fmt.Println() + fmt.Println(" Next steps:") + fmt.Println(" micro run \033[2m# start all services\033[0m") + fmt.Println(" micro chat --provider anthropic \033[2m# talk to them\033[0m") + fmt.Println() + return nil +} diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index fb10ecca9c..3aea6c8d03 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -19,6 +19,7 @@ import ( "github.com/urfave/cli/v2" "go-micro.dev/v5/cmd" + "go-micro.dev/v5/cmd/micro/cli/generate" "go-micro.dev/v5/cmd/micro/run/config" "go-micro.dev/v5/cmd/micro/run/watcher" "go-micro.dev/v5/cmd/micro/server" @@ -175,6 +176,11 @@ func waitForHealth(port int, timeout time.Duration) bool { } func Run(c *cli.Context) error { + // Handle --prompt: generate services first, then run them + if prompt := c.String("prompt"); prompt != "" { + return runWithPrompt(c, prompt) + } + dir := c.Args().Get(0) if dir == "" { dir = "." @@ -520,6 +526,72 @@ Examples: Usage: "MCP gateway address (e.g., :3000). Enables MCP protocol for AI tools.", EnvVars: []string{"MICRO_MCP_ADDRESS"}, }, + &cli.StringFlag{ + Name: "prompt", + Usage: "Describe a system to generate and run (uses AI to design services)", + EnvVars: []string{"MICRO_RUN_PROMPT"}, + }, + &cli.StringFlag{ + Name: "provider", + Usage: "AI provider for --prompt", + EnvVars: []string{"MICRO_AI_PROVIDER"}, + }, + &cli.StringFlag{ + Name: "api_key", + Usage: "API key for --prompt", + EnvVars: []string{"MICRO_AI_API_KEY"}, + }, }, }) } + +func runWithPrompt(c *cli.Context, prompt string) error { + provider := c.String("provider") + apiKey := c.String("api_key") + if apiKey == "" { + for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", + "ATLASCLOUD_API_KEY", "GROQ_API_KEY", "MISTRAL_API_KEY", "TOGETHER_API_KEY", "MICRO_AI_API_KEY"} { + if v := os.Getenv(env); v != "" { + apiKey = v + break + } + } + } + if apiKey == "" { + return fmt.Errorf("--api_key or a provider API key env var is required for --prompt") + } + + fmt.Println() + fmt.Println(" \033[1mmicro run --prompt\033[0m") + fmt.Println() + fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt) + + design, err := generate.Design(context.Background(), provider, apiKey, "", prompt) + if err != nil { + return fmt.Errorf("design failed: %w", err) + } + + fmt.Println(" Services:") + for _, svc := range design.Services { + fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description) + } + fmt.Println() + + fmt.Println(" Generating code...") + if err := generate.Generate(".", design); err != nil { + return fmt.Errorf("generate failed: %w", err) + } + for _, svc := range design.Services { + fmt.Printf(" \033[32m✓\033[0m %s/\n", svc.Name) + } + fmt.Println() + + // Now run normally — micro run discovers the generated services + fmt.Println(" Starting services...") + fmt.Println() + + // Run normally from current directory (services are now generated) + // Set the prompt to empty via a new context so Run doesn't recurse + c.Set("prompt", "") + return Run(c) +} From b57d3bb453bb830d1b72ea90122281a9dc9ce42a Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 11:26:51 +0000 Subject: [PATCH 02/19] feat: LLM generates real business logic with compile-fix loop MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Rebuild the generate package so the LLM writes actual handler code with business logic, not just CRUD scaffolding. The flow is now: 1. LLM designs architecture (service names, fields, endpoints) → returns structured JSON 2. Proto, main.go, go.mod, Makefile generated deterministically from the design (guaranteed to be correct) 3. go mod tidy + make proto compiles the protos 4. LLM generates handler code with REAL business logic → given the proto, endpoint descriptions, and go-micro patterns 5. go build — does it compile? 6. If no: feed errors back to LLM, get fixed code (up to 3 attempts) 7. If yes: service is ready The handler prompt instructs the LLM to: - Use sync.RWMutex for thread-safe in-memory state - Include validation, edge cases, meaningful errors - Write doc comments with @example tags for MCP - Implement actual domain logic, not just map operations Proto generation still uses deterministic templates (CRUD + custom endpoints from the design spec) to guarantee correctness. The compile-fix loop catches LLM mistakes automatically. Both micro new --prompt and micro run --prompt use this flow. --- cmd/micro/cli/generate/generate.go | 464 +++++++++++++++-------------- cmd/micro/cli/new/new.go | 2 +- cmd/micro/run/run.go | 2 +- 3 files changed, 246 insertions(+), 222 deletions(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 89d1dd67e6..66ccf6c842 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -1,6 +1,6 @@ -// Package generate implements 'micro new --prompt' and 'micro run --prompt' -// which use an LLM to design services from a natural language description, -// then generate real go-micro code using the existing template system. +// Package generate implements AI-powered service generation for go-micro. +// It uses an LLM to design service architecture and generate handler code +// with real business logic, then compiles and fixes errors iteratively. package generate import ( @@ -14,7 +14,6 @@ import ( "go-micro.dev/v5/ai" - // Register providers so ai.New works. _ "go-micro.dev/v5/ai/anthropic" _ "go-micro.dev/v5/ai/atlascloud" _ "go-micro.dev/v5/ai/gemini" @@ -24,9 +23,10 @@ import ( _ "go-micro.dev/v5/ai/together" ) -const designPrompt = `You are a Go microservices architect. Given a description of a system, design the services needed. +const designPrompt = `You are a Go microservices architect using the go-micro framework. +Given a system description, design the services needed. -Return ONLY valid JSON with this exact structure: +Return ONLY valid JSON: { "services": [ { @@ -43,21 +43,45 @@ Return ONLY valid JSON with this exact structure: } Rules: -- Service names are lowercase, hyphenated (e.g. "order-items") -- Each service should have CRUD endpoints (Create, Read, Update, Delete, List) plus any custom ones -- Field types must be: string, int64, bool, float64 -- Every service must have an "id" field (string) and "created"/"updated" fields (int64) -- Endpoint names are PascalCase (e.g. "Create", "FindByEmail") -- The example field shows a realistic JSON input for the endpoint -- Keep it focused — 2-5 services max -- Each endpoint description should be clear enough for an AI agent to understand when to call it` - -// ServiceDesign is the LLM's output describing the system architecture. +- Service names are lowercase, hyphenated +- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List +- Add custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory, CalculateShipping) +- Field types: string, int64, bool, float64 +- Every service needs id (string), created (int64), updated (int64) fields +- Endpoint names are PascalCase +- Examples should be realistic JSON +- 2-5 services max, focused on the domain` + +const handlerPrompt = `You are a Go developer writing a handler for a go-micro service. +Generate a COMPLETE, COMPILABLE Go handler file. + +The handler must: +1. Use package "handler" +2. Import the proto package as: pb "%s/proto" +3. Import go-micro logger as: log "go-micro.dev/v5/logger" +4. Import "github.com/google/uuid" for ID generation +5. Use sync.RWMutex for thread-safe in-memory storage +6. Include REAL business logic — not just CRUD map operations +7. Every exported method must have a doc comment explaining what it does +8. Every method must have an @example tag with realistic JSON input +9. Handle edge cases, validation, and return meaningful errors + +The struct name is %s. +The constructor is func New() *%s. + +Here is the proto definition: +%s + +Here is what each endpoint should do: +%s + +Return ONLY the Go code. No markdown, no explanation. Just the .go file content starting with "package handler".` + +// ServiceDesign is the LLM's output. type ServiceDesign struct { Services []ServiceSpec `json:"services"` } -// ServiceSpec describes one service to generate. type ServiceSpec struct { Name string `json:"name"` Description string `json:"description"` @@ -65,33 +89,21 @@ type ServiceSpec struct { Endpoints []EndpointSpec `json:"endpoints"` } -// FieldSpec describes a field on the service's record type. type FieldSpec struct { Name string `json:"name"` Type string `json:"type"` Description string `json:"description"` } -// EndpointSpec describes an RPC endpoint. type EndpointSpec struct { Name string `json:"name"` Description string `json:"description"` Example string `json:"example"` } -// Design calls an LLM to design services from a natural language prompt. +// Design calls an LLM to design services from a prompt. func Design(ctx context.Context, provider, apiKey, model, prompt string) (*ServiceDesign, error) { - if provider == "" { - provider = ai.AutoDetectProvider("") - } - - var opts []ai.Option - opts = append(opts, ai.WithAPIKey(apiKey)) - if model != "" { - opts = append(opts, ai.WithModel(model)) - } - - m := ai.New(provider, opts...) + m := newModel(provider, apiKey, model) if m == nil { return nil, fmt.Errorf("unknown provider: %s", provider) } @@ -101,265 +113,245 @@ func Design(ctx context.Context, provider, apiKey, model, prompt string) (*Servi SystemPrompt: designPrompt, }) if err != nil { - return nil, fmt.Errorf("LLM design failed: %w", err) - } - - reply := resp.Reply - if resp.Answer != "" { - reply = resp.Answer + return nil, fmt.Errorf("design failed: %w", err) } - // Extract JSON from the response (LLM may wrap it in markdown) + reply := firstNonEmpty(resp.Answer, resp.Reply) reply = extractJSON(reply) var design ServiceDesign if err := json.Unmarshal([]byte(reply), &design); err != nil { - return nil, fmt.Errorf("failed to parse LLM response as JSON: %w\nResponse: %s", err, reply) + return nil, fmt.Errorf("failed to parse design: %w\nResponse: %s", err, reply) } - if len(design.Services) == 0 { - return nil, fmt.Errorf("LLM returned no services") + return nil, fmt.Errorf("no services designed") } - return &design, nil } // Generate creates go-micro service directories from a design. -// Each service gets proto, handler, main.go, Makefile, go.mod. -func Generate(baseDir string, design *ServiceDesign) error { - for _, svc := range design.Services { +func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provider, apiKey, model string) error { + m := newModel(provider, apiKey, model) + + for i, svc := range design.Services { svcDir := filepath.Join(baseDir, svc.Name) - if err := generateService(svcDir, svc); err != nil { - return fmt.Errorf("generate %s: %w", svc.Name, err) + fmt.Printf(" \033[2m[%d/%d]\033[0m generating \033[36m%s\033[0m...\n", i+1, len(design.Services), svc.Name) + + // Step 1: Generate proto (deterministic — from design spec) + if err := generateStructure(svcDir, svc); err != nil { + return fmt.Errorf("structure %s: %w", svc.Name, err) + } + + // Step 2: Run go mod tidy + make proto to get compiled proto + runIn(svcDir, "go", "mod", "tidy") + runIn(svcDir, "make", "proto") + + // Step 3: Generate handler with business logic (LLM) + proto := readFile(filepath.Join(svcDir, "proto", svc.Name+".proto")) + if err := generateHandler(ctx, m, svcDir, svc, proto); err != nil { + return fmt.Errorf("handler %s: %w", svc.Name, err) + } + + // Step 4: Compile-fix loop + if err := compileFix(ctx, m, svcDir, svc.Name, 3); err != nil { + fmt.Printf(" \033[33m⚠\033[0m %s has compile errors (may need manual fix)\n", svc.Name) + } else { + fmt.Printf(" \033[32m✓\033[0m %s\n", svc.Name) } } return nil } -func generateService(dir string, svc ServiceSpec) error { - if err := os.MkdirAll(filepath.Join(dir, "handler"), 0755); err != nil { - return err - } - if err := os.MkdirAll(filepath.Join(dir, "proto"), 0755); err != nil { - return err - } +// generateStructure creates the proto, main.go, go.mod, Makefile. +func generateStructure(dir string, svc ServiceSpec) error { + os.MkdirAll(filepath.Join(dir, "handler"), 0755) + os.MkdirAll(filepath.Join(dir, "proto"), 0755) name := svc.Name titleName := toTitle(name) + dehyphen := strings.ReplaceAll(name, "-", "") // Proto - proto := generateProto(name, titleName, svc) - if err := os.WriteFile(filepath.Join(dir, "proto", name+".proto"), []byte(proto), 0644); err != nil { - return err - } + writeFile(filepath.Join(dir, "proto", name+".proto"), buildProto(dehyphen, titleName, svc)) - // Handler - handler := generateHandler(name, titleName, svc) - if err := os.WriteFile(filepath.Join(dir, "handler", name+".go"), []byte(handler), 0644); err != nil { - return err + // Main + writeFile(filepath.Join(dir, "main.go"), buildMain(name, titleName)) + + // Makefile + writeFile(filepath.Join(dir, "Makefile"), + "GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") + + // go.mod + writeFile(filepath.Join(dir, "go.mod"), + fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name)) + + // Placeholder handler so go mod tidy works + writeFile(filepath.Join(dir, "handler", name+".go"), + fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName)) + + return nil +} + +// generateHandler asks the LLM to write the handler with business logic. +func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpec, proto string) error { + if m == nil { + return nil // no LLM — keep the placeholder } - // Main - main := generateMain(name, titleName) - if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(main), 0644); err != nil { - return err + titleName := toTitle(svc.Name) + + // Build endpoint descriptions + var epDescs []string + for _, ep := range svc.Endpoints { + epDescs = append(epDescs, fmt.Sprintf("- %s: %s (example input: %s)", ep.Name, ep.Description, ep.Example)) } - // Makefile - makefile := fmt.Sprintf("GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") - if err := os.WriteFile(filepath.Join(dir, "Makefile"), []byte(makefile), 0644); err != nil { + prompt := fmt.Sprintf(handlerPrompt, + svc.Name, titleName, titleName, proto, strings.Join(epDescs, "\n")) + + resp, err := m.Generate(ctx, &ai.Request{ + Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic.", svc.Name), + SystemPrompt: prompt, + }) + if err != nil { return err } - // go.mod - gomod := fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name) - if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(gomod), 0644); err != nil { - return err + code := firstNonEmpty(resp.Answer, resp.Reply) + code = extractCode(code) + + if !strings.HasPrefix(strings.TrimSpace(code), "package") { + return fmt.Errorf("LLM did not return valid Go code") } - // Run go mod tidy and make proto - runIn(dir, "go", "mod", "tidy") - runIn(dir, "make", "proto") + writeFile(filepath.Join(dir, "handler", svc.Name+".go"), code) + return nil +} +// compileFix tries to compile, and if it fails, sends the error to +// the LLM to fix. Up to maxAttempts iterations. +func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts int) error { + for attempt := 0; attempt < maxAttempts; attempt++ { + cmd := exec.Command("go", "build", "./...") + cmd.Dir = dir + out, err := cmd.CombinedOutput() + if err == nil { + return nil // compiles! + } + + if m == nil { + return fmt.Errorf("compile failed: %s", string(out)) + } + + // Read current handler + handlerPath := filepath.Join(dir, "handler", name+".go") + currentCode := readFile(handlerPath) + + fmt.Printf(" \033[33m→\033[0m compile error, fixing (attempt %d/%d)...\n", attempt+1, maxAttempts) + + resp, fixErr := m.Generate(ctx, &ai.Request{ + Prompt: fmt.Sprintf("This Go code has compile errors. Fix ALL of them and return the COMPLETE corrected file.\n\nErrors:\n%s\n\nCode:\n%s", + string(out), currentCode), + SystemPrompt: "You are a Go expert. Return ONLY the corrected Go code. No markdown, no explanation. Start with 'package handler'.", + }) + if fixErr != nil { + return fmt.Errorf("fix attempt failed: %w", fixErr) + } + + fixed := firstNonEmpty(resp.Answer, resp.Reply) + fixed = extractCode(fixed) + if strings.HasPrefix(strings.TrimSpace(fixed), "package") { + writeFile(handlerPath, fixed) + } + } + + // Final check + cmd := exec.Command("go", "build", "./...") + cmd.Dir = dir + if out, err := cmd.CombinedOutput(); err != nil { + return fmt.Errorf("still fails after %d attempts: %s", maxAttempts, string(out)) + } return nil } -func generateProto(name, titleName string, svc ServiceSpec) string { - dehyphen := strings.ReplaceAll(name, "-", "") +func newModel(provider, apiKey, model string) ai.Model { + if provider == "" { + provider = ai.AutoDetectProvider("") + } + var opts []ai.Option + opts = append(opts, ai.WithAPIKey(apiKey)) + if model != "" { + opts = append(opts, ai.WithModel(model)) + } + return ai.New(provider, opts...) +} + +func buildProto(dehyphen, titleName string, svc ServiceSpec) string { var b strings.Builder b.WriteString(fmt.Sprintf("syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen)) - // Service definition b.WriteString(fmt.Sprintf("service %s {\n", titleName)) for _, ep := range svc.Endpoints { - reqName := ep.Name + "Request" - rspName := ep.Name + "Response" - if ep.Name == "List" { - reqName = "ListRequest" - rspName = "ListResponse" - } - b.WriteString(fmt.Sprintf("\trpc %s(%s) returns (%s) {}\n", ep.Name, reqName, rspName)) + b.WriteString(fmt.Sprintf("\trpc %s(%sRequest) returns (%sResponse) {}\n", ep.Name, ep.Name, ep.Name)) } b.WriteString("}\n\n") // Record message b.WriteString(fmt.Sprintf("message %sRecord {\n", titleName)) - fieldNum := 1 - for _, f := range svc.Fields { - b.WriteString(fmt.Sprintf("\t// %s\n", f.Description)) - b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fieldNum)) - fieldNum++ + for i, f := range svc.Fields { + b.WriteString(fmt.Sprintf("\t%s %s = %d; // %s\n", protoType(f.Type), f.Name, i+1, f.Description)) } b.WriteString("}\n\n") - // Request/response messages for each endpoint + // Request/response for each endpoint for _, ep := range svc.Endpoints { switch ep.Name { case "Create": b.WriteString(fmt.Sprintf("message CreateRequest {\n")) - fn := 1 + n := 1 for _, f := range svc.Fields { if f.Name == "id" || f.Name == "created" || f.Name == "updated" { continue } - b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) - fn++ + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n)) + n++ } - b.WriteString("}\n\n") - b.WriteString(fmt.Sprintf("message CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + b.WriteString(fmt.Sprintf("}\n\nmessage CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) case "Read": - b.WriteString("message ReadRequest {\n\tstring id = 1;\n}\n\n") - b.WriteString(fmt.Sprintf("message ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + b.WriteString(fmt.Sprintf("message ReadRequest {\n\tstring id = 1;\n}\n\nmessage ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) case "Update": b.WriteString("message UpdateRequest {\n\tstring id = 1;\n") - fn := 2 + n := 2 for _, f := range svc.Fields { if f.Name == "id" || f.Name == "created" || f.Name == "updated" { continue } - b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) - fn++ + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n)) + n++ } - b.WriteString("}\n\n") - b.WriteString(fmt.Sprintf("message UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) + b.WriteString(fmt.Sprintf("}\n\nmessage UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)) case "Delete": - b.WriteString("message DeleteRequest {\n\tstring id = 1;\n}\n\n") - b.WriteString("message DeleteResponse {\n\tbool deleted = 1;\n}\n\n") + b.WriteString(fmt.Sprintf("message DeleteRequest {\n\tstring id = 1;\n}\n\nmessage DeleteResponse {\n\tbool deleted = 1;\n}\n\n")) case "List": - b.WriteString("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n}\n\n") - b.WriteString(fmt.Sprintf("message ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName)) + b.WriteString(fmt.Sprintf("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n\tstring query = 3;\n}\n\nmessage ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName)) default: - // Custom endpoint — request has the fields, response has a result string + // Custom endpoint — use all fields as input, record as output b.WriteString(fmt.Sprintf("message %sRequest {\n", ep.Name)) - fn := 1 + n := 1 for _, f := range svc.Fields { if f.Name == "created" || f.Name == "updated" { continue } - b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, fn)) - fn++ + b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n)) + n++ } - b.WriteString("}\n\n") - b.WriteString(fmt.Sprintf("message %sResponse {\n\tstring result = 1;\n}\n\n", ep.Name)) + b.WriteString(fmt.Sprintf("}\n\nmessage %sResponse {\n\t%sRecord record = 1;\n\tstring message = 2;\n\tbool success = 3;\n}\n\n", ep.Name, titleName)) } } - return b.String() } -func generateHandler(name, titleName string, svc ServiceSpec) string { - dehyphen := strings.ReplaceAll(name, "-", "") - var b strings.Builder - - b.WriteString("package handler\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"sort\"\n\t\"sync\"\n\t\"time\"\n\n") - b.WriteString("\t\"github.com/google/uuid\"\n") - b.WriteString(fmt.Sprintf("\tlog \"go-micro.dev/v5/logger\"\n\n\tpb \"%s/proto\"\n)\n\n", name)) - - b.WriteString(fmt.Sprintf("type %s struct {\n\tmu sync.RWMutex\n\trecords map[string]*pb.%sRecord\n}\n\n", titleName, titleName)) - b.WriteString(fmt.Sprintf("func New() *%s {\n\treturn &%s{records: make(map[string]*pb.%sRecord)}\n}\n\n", titleName, titleName, titleName)) - - for _, ep := range svc.Endpoints { - example := ep.Example - if example == "" { - example = "{}" - } - b.WriteString(fmt.Sprintf("// %s %s\n//\n// @example %s\n", ep.Name, ep.Description, example)) - - switch ep.Name { - case "Create": - b.WriteString(fmt.Sprintf("func (h *%s) Create(ctx context.Context, req *pb.CreateRequest, rsp *pb.CreateResponse) error {\n", titleName)) - b.WriteString(fmt.Sprintf("\tlog.Infof(\"Creating %s record\")\n", dehyphen)) - b.WriteString("\tnow := time.Now().Unix()\n") - b.WriteString(fmt.Sprintf("\trecord := &pb.%sRecord{\n\t\tId: uuid.New().String(),\n", titleName)) - for _, f := range svc.Fields { - if f.Name == "id" || f.Name == "created" || f.Name == "updated" { - continue - } - b.WriteString(fmt.Sprintf("\t\t%s: req.%s,\n", toTitle(f.Name), toTitle(f.Name))) - } - b.WriteString("\t\tCreated: now,\n\t\tUpdated: now,\n\t}\n") - b.WriteString("\th.mu.Lock()\n\th.records[record.Id] = record\n\th.mu.Unlock()\n\trsp.Record = record\n\treturn nil\n}\n\n") - - case "Read": - b.WriteString(fmt.Sprintf("func (h *%s) Read(ctx context.Context, req *pb.ReadRequest, rsp *pb.ReadResponse) error {\n", titleName)) - b.WriteString("\th.mu.RLock()\n\trecord, ok := h.records[req.Id]\n\th.mu.RUnlock()\n") - b.WriteString("\tif !ok {\n\t\treturn fmt.Errorf(\"record %s not found\", req.Id)\n\t}\n") - b.WriteString("\trsp.Record = record\n\treturn nil\n}\n\n") - - case "Update": - b.WriteString(fmt.Sprintf("func (h *%s) Update(ctx context.Context, req *pb.UpdateRequest, rsp *pb.UpdateResponse) error {\n", titleName)) - b.WriteString("\th.mu.Lock()\n\tdefer h.mu.Unlock()\n") - b.WriteString("\trecord, ok := h.records[req.Id]\n") - b.WriteString("\tif !ok {\n\t\treturn fmt.Errorf(\"record %s not found\", req.Id)\n\t}\n") - for _, f := range svc.Fields { - if f.Name == "id" || f.Name == "created" || f.Name == "updated" { - continue - } - switch f.Type { - case "string": - b.WriteString(fmt.Sprintf("\tif req.%s != \"\" {\n\t\trecord.%s = req.%s\n\t}\n", toTitle(f.Name), toTitle(f.Name), toTitle(f.Name))) - case "int64": - b.WriteString(fmt.Sprintf("\tif req.%s != 0 {\n\t\trecord.%s = req.%s\n\t}\n", toTitle(f.Name), toTitle(f.Name), toTitle(f.Name))) - case "bool": - b.WriteString(fmt.Sprintf("\trecord.%s = req.%s\n", toTitle(f.Name), toTitle(f.Name))) - default: - b.WriteString(fmt.Sprintf("\trecord.%s = req.%s\n", toTitle(f.Name), toTitle(f.Name))) - } - } - b.WriteString("\trecord.Updated = time.Now().Unix()\n\trsp.Record = record\n\treturn nil\n}\n\n") - - case "Delete": - b.WriteString(fmt.Sprintf("func (h *%s) Delete(ctx context.Context, req *pb.DeleteRequest, rsp *pb.DeleteResponse) error {\n", titleName)) - b.WriteString("\th.mu.Lock()\n\t_, ok := h.records[req.Id]\n\tif ok {\n\t\tdelete(h.records, req.Id)\n\t}\n\th.mu.Unlock()\n") - b.WriteString("\trsp.Deleted = ok\n\treturn nil\n}\n\n") - - case "List": - b.WriteString(fmt.Sprintf("func (h *%s) List(ctx context.Context, req *pb.ListRequest, rsp *pb.ListResponse) error {\n", titleName)) - b.WriteString(fmt.Sprintf("\th.mu.RLock()\n\tdefer h.mu.RUnlock()\n\tall := make([]*pb.%sRecord, 0, len(h.records))\n", titleName)) - b.WriteString("\tfor _, r := range h.records {\n\t\tall = append(all, r)\n\t}\n") - b.WriteString("\tsort.Slice(all, func(i, j int) bool { return all[i].Created > all[j].Created })\n") - b.WriteString("\trsp.Total = int64(len(all))\n") - b.WriteString("\toffset := int(req.Offset)\n\tif offset > len(all) { offset = len(all) }\n") - b.WriteString("\tlimit := int(req.Limit)\n\tif limit <= 0 { limit = 20 }\n") - b.WriteString("\tend := offset + limit\n\tif end > len(all) { end = len(all) }\n") - b.WriteString("\trsp.Records = all[offset:end]\n\treturn nil\n}\n\n") - - default: - // Custom endpoint — simple stub - b.WriteString(fmt.Sprintf("func (h *%s) %s(ctx context.Context, req *pb.%sRequest, rsp *pb.%sResponse) error {\n", - titleName, ep.Name, ep.Name, ep.Name)) - b.WriteString(fmt.Sprintf("\tlog.Infof(\"%s.%s called\")\n", titleName, ep.Name)) - b.WriteString("\trsp.Result = \"ok\"\n\treturn nil\n}\n\n") - } - } - - // Suppress unused imports - b.WriteString("var _ = fmt.Sprintf\nvar _ = sort.Slice\n") - - return b.String() -} - -func generateMain(name, titleName string) string { +func buildMain(name, titleName string) string { return fmt.Sprintf(`package main import ( @@ -374,18 +366,14 @@ func main() { service := micro.New("%s", mcp.WithMCP(":3001"), ) - service.Init() - pb.Register%sHandler(service.Server(), handler.New()) - service.Run() } `, name, name, strings.ReplaceAll(name, "-", ""), titleName) } func extractJSON(s string) string { - // Try to find JSON in markdown code blocks if i := strings.Index(s, "```json"); i >= 0 { s = s[i+7:] if j := strings.Index(s, "```"); j >= 0 { @@ -398,13 +386,13 @@ func extractJSON(s string) string { return strings.TrimSpace(s[:j]) } } - // Try to find raw JSON if i := strings.Index(s, "{"); i >= 0 { depth := 0 for j := i; j < len(s); j++ { - if s[j] == '{' { + switch s[j] { + case '{': depth++ - } else if s[j] == '}' { + case '}': depth-- if depth == 0 { return s[i : j+1] @@ -415,8 +403,28 @@ func extractJSON(s string) string { return s } -func protoType(goType string) string { - switch goType { +func extractCode(s string) string { + if i := strings.Index(s, "```go"); i >= 0 { + s = s[i+5:] + if j := strings.Index(s, "```"); j >= 0 { + return strings.TrimSpace(s[:j]) + } + } + if i := strings.Index(s, "```"); i >= 0 { + s = s[i+3:] + if j := strings.Index(s, "```"); j >= 0 { + return strings.TrimSpace(s[:j]) + } + } + // Try to find raw package declaration + if i := strings.Index(s, "package "); i >= 0 { + return strings.TrimSpace(s[i:]) + } + return strings.TrimSpace(s) +} + +func protoType(t string) string { + switch t { case "int64": return "int64" case "int32": @@ -431,9 +439,7 @@ func protoType(goType string) string { } func toTitle(s string) string { - s = strings.ReplaceAll(s, "-", " ") - s = strings.ReplaceAll(s, "_", " ") - words := strings.Fields(s) + words := strings.FieldsFunc(s, func(r rune) bool { return r == '-' || r == '_' || r == ' ' }) for i, w := range words { if len(w) > 0 { words[i] = strings.ToUpper(w[:1]) + w[1:] @@ -442,6 +448,24 @@ func toTitle(s string) string { return strings.Join(words, "") } +func firstNonEmpty(vals ...string) string { + for _, v := range vals { + if v != "" { + return v + } + } + return "" +} + +func readFile(path string) string { + b, _ := os.ReadFile(path) + return string(b) +} + +func writeFile(path, content string) { + os.WriteFile(path, []byte(content), 0644) +} + func runIn(dir string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Dir = dir diff --git a/cmd/micro/cli/new/new.go b/cmd/micro/cli/new/new.go index 7fe89d382f..ae4e4d36f3 100644 --- a/cmd/micro/cli/new/new.go +++ b/cmd/micro/cli/new/new.go @@ -353,7 +353,7 @@ func runPrompt(ctx *cli.Context, prompt string) error { fmt.Println() fmt.Println(" Generating code...") - if err := generate.Generate(".", design); err != nil { + if err := generate.Generate(context.Background(), ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) } diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index 3aea6c8d03..25e70fcd1a 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -578,7 +578,7 @@ func runWithPrompt(c *cli.Context, prompt string) error { fmt.Println() fmt.Println(" Generating code...") - if err := generate.Generate(".", design); err != nil { + if err := generate.Generate(context.Background(), ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) } for _, svc := range design.Services { From 4849011a292b4a48d3408e3faa56465cd6762e02 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 12:01:12 +0000 Subject: [PATCH 03/19] fix: handle edge cases in prompt-based generation - Fix PATH for protoc-gen-micro in child processes - Handle existing directories: skip structural files (main.go, go.mod, Makefile) if dir exists, always regenerate proto, only write placeholder handler if none exists - Allow re-running micro new --prompt on same directory to iterate on business logic without clobbering user edits Tested end-to-end: "a simple todo list with tasks and categories" generates 2 services (task-service, category-service) with real business logic (validation, toggle complete, etc.), compiles after 1 fix iteration, and runs with 6 MCP tools discovered. --- cmd/micro/cli/generate/generate.go | 36 ++++++++++++++++++++---------- 1 file changed, 24 insertions(+), 12 deletions(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 66ccf6c842..2aaed2b01c 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -130,6 +130,8 @@ func Design(ctx context.Context, provider, apiKey, model, prompt string) (*Servi } // Generate creates go-micro service directories from a design. +// If a service directory already exists, it skips structure generation +// but regenerates the handler (allowing iterative improvement). func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provider, apiKey, model string) error { m := newModel(provider, apiKey, model) @@ -163,7 +165,13 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid } // generateStructure creates the proto, main.go, go.mod, Makefile. +// If the directory already exists, only regenerates the proto +// (handler will be regenerated separately by the LLM). func generateStructure(dir string, svc ServiceSpec) error { + exists := false + if _, err := os.Stat(dir); err == nil { + exists = true + } os.MkdirAll(filepath.Join(dir, "handler"), 0755) os.MkdirAll(filepath.Join(dir, "proto"), 0755) @@ -171,23 +179,26 @@ func generateStructure(dir string, svc ServiceSpec) error { titleName := toTitle(name) dehyphen := strings.ReplaceAll(name, "-", "") - // Proto + // Always regenerate proto (design may have changed) writeFile(filepath.Join(dir, "proto", name+".proto"), buildProto(dehyphen, titleName, svc)) - // Main - writeFile(filepath.Join(dir, "main.go"), buildMain(name, titleName)) + // Only write structural files if directory is new + if !exists { + writeFile(filepath.Join(dir, "main.go"), buildMain(name, titleName)) - // Makefile - writeFile(filepath.Join(dir, "Makefile"), - "GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") + writeFile(filepath.Join(dir, "Makefile"), + "GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") - // go.mod - writeFile(filepath.Join(dir, "go.mod"), - fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name)) + writeFile(filepath.Join(dir, "go.mod"), + fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name)) + } - // Placeholder handler so go mod tidy works - writeFile(filepath.Join(dir, "handler", name+".go"), - fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName)) + // Placeholder handler so go mod tidy works (will be overwritten by LLM) + handlerPath := filepath.Join(dir, "handler", name+".go") + if _, err := os.Stat(handlerPath); os.IsNotExist(err) { + writeFile(handlerPath, + fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName)) + } return nil } @@ -469,6 +480,7 @@ func writeFile(path, content string) { func runIn(dir string, name string, args ...string) error { cmd := exec.Command(name, args...) cmd.Dir = dir + cmd.Env = append(os.Environ(), "PATH="+os.Getenv("PATH")+":"+os.Getenv("GOPATH")+"/bin:"+os.Getenv("HOME")+"/go/bin") cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr return cmd.Run() From 703d1f63051010cdff4222ccac7468ba116b81cd Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 14:54:47 +0000 Subject: [PATCH 04/19] feat: auto-detect modified handlers on regeneration Instead of requiring a --keep-handlers flag, the generate package now tracks a SHA-256 hash of each generated handler in a .micro metadata file. On re-run, if the user has edited the handler since generation, it's left untouched. Unmodified handlers are regenerated normally. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/generate/generate.go | 61 +++++++++++++++++++++++++++- cmd/micro/cli/new/template/ignore.go | 1 + 2 files changed, 61 insertions(+), 1 deletion(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 2aaed2b01c..1a947abfd8 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -5,6 +5,8 @@ package generate import ( "context" + "crypto/sha256" + "encoding/hex" "encoding/json" "fmt" "os" @@ -160,6 +162,9 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid } else { fmt.Printf(" \033[32m✓\033[0m %s\n", svc.Name) } + + // Record final handler hash (after any compile fixes) + recordHandlerHash(svcDir, filepath.Join(svcDir, "handler", svc.Name+".go")) } return nil } @@ -198,17 +203,27 @@ func generateStructure(dir string, svc ServiceSpec) error { if _, err := os.Stat(handlerPath); os.IsNotExist(err) { writeFile(handlerPath, fmt.Sprintf("package handler\n\ntype %s struct{}\n\nfunc New() *%s { return &%s{} }\n", titleName, titleName, titleName)) + recordHandlerHash(dir, handlerPath) } return nil } // generateHandler asks the LLM to write the handler with business logic. +// If the handler exists and the user has modified it since generation, +// it is left untouched. func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpec, proto string) error { if m == nil { return nil // no LLM — keep the placeholder } + handlerFile := filepath.Join(dir, "handler", svc.Name+".go") + + if handlerModified(dir, handlerFile) { + fmt.Printf(" \033[2mkeeping %s handler (modified)\033[0m\n", svc.Name) + return nil + } + titleName := toTitle(svc.Name) // Build endpoint descriptions @@ -235,7 +250,8 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe return fmt.Errorf("LLM did not return valid Go code") } - writeFile(filepath.Join(dir, "handler", svc.Name+".go"), code) + writeFile(handlerFile, code) + recordHandlerHash(dir, handlerFile) return nil } @@ -485,3 +501,46 @@ func runIn(dir string, name string, args ...string) error { cmd.Stderr = os.Stderr return cmd.Run() } + +func fileHash(path string) string { + b, err := os.ReadFile(path) + if err != nil { + return "" + } + h := sha256.Sum256(b) + return hex.EncodeToString(h[:]) +} + +func metaPath(svcDir string) string { + return filepath.Join(svcDir, ".micro") +} + +func readMeta(svcDir string) map[string]string { + m := make(map[string]string) + b, err := os.ReadFile(metaPath(svcDir)) + if err != nil { + return m + } + json.Unmarshal(b, &m) + return m +} + +func writeMeta(svcDir string, m map[string]string) { + b, _ := json.MarshalIndent(m, "", " ") + os.WriteFile(metaPath(svcDir), b, 0644) +} + +func handlerModified(svcDir, handlerFile string) bool { + meta := readMeta(svcDir) + savedHash, ok := meta["handler_hash"] + if !ok { + return false // no hash recorded — treat as unmodified (first run or pre-tracking) + } + return fileHash(handlerFile) != savedHash +} + +func recordHandlerHash(svcDir, handlerFile string) { + meta := readMeta(svcDir) + meta["handler_hash"] = fileHash(handlerFile) + writeMeta(svcDir, meta) +} diff --git a/cmd/micro/cli/new/template/ignore.go b/cmd/micro/cli/new/template/ignore.go index 6e2d569e31..9a803266c1 100644 --- a/cmd/micro/cli/new/template/ignore.go +++ b/cmd/micro/cli/new/template/ignore.go @@ -3,5 +3,6 @@ package template var ( GitIgnore = ` {{.Alias}} +.micro ` ) From 18c878b88218d5f2e3534c13668ee56ee668da46 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:03:13 +0000 Subject: [PATCH 05/19] feat: add tests, fix go.mod, gitignore, proto tracking, spinner - Add 12 tests covering helpers, proto generation, hash tracking - Fix go.mod: write minimal module file, let go mod tidy resolve deps - Add .gitignore to prompt-generated services - Protect user-edited proto files (same hash tracking as handlers) - Add spinner during LLM calls so it doesn't look hung https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/generate/generate.go | 79 +++++- cmd/micro/cli/generate/generate_test.go | 343 ++++++++++++++++++++++++ 2 files changed, 411 insertions(+), 11 deletions(-) create mode 100644 cmd/micro/cli/generate/generate_test.go diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 1a947abfd8..f94cad588a 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -13,6 +13,8 @@ import ( "os/exec" "path/filepath" "strings" + "sync" + "time" "go-micro.dev/v5/ai" @@ -110,10 +112,12 @@ func Design(ctx context.Context, provider, apiKey, model, prompt string) (*Servi return nil, fmt.Errorf("unknown provider: %s", provider) } + sp := startSpinner("designing services...") resp, err := m.Generate(ctx, &ai.Request{ Prompt: fmt.Sprintf("Design a microservices system for: %s", prompt), SystemPrompt: designPrompt, }) + sp.Stop() if err != nil { return nil, fmt.Errorf("design failed: %w", err) } @@ -184,8 +188,14 @@ func generateStructure(dir string, svc ServiceSpec) error { titleName := toTitle(name) dehyphen := strings.ReplaceAll(name, "-", "") - // Always regenerate proto (design may have changed) - writeFile(filepath.Join(dir, "proto", name+".proto"), buildProto(dehyphen, titleName, svc)) + // Regenerate proto unless user has modified it + protoPath := filepath.Join(dir, "proto", name+".proto") + if !fileModified(dir, "proto_hash", protoPath) { + writeFile(protoPath, buildProto(dehyphen, titleName, svc)) + recordFileHash(dir, "proto_hash", protoPath) + } else { + fmt.Printf(" \033[2mkeeping %s proto (modified)\033[0m\n", name) + } // Only write structural files if directory is new if !exists { @@ -195,7 +205,10 @@ func generateStructure(dir string, svc ServiceSpec) error { "GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") writeFile(filepath.Join(dir, "go.mod"), - fmt.Sprintf("module %s\n\ngo 1.22\n\nrequire (\n\tgo-micro.dev/v5 latest\n\tgithub.com/golang/protobuf latest\n\tgoogle.golang.org/protobuf latest\n)\n", name)) + fmt.Sprintf("module %s\n\ngo 1.22\n", name)) + + writeFile(filepath.Join(dir, ".gitignore"), + fmt.Sprintf("%s\n.micro\n", name)) } // Placeholder handler so go mod tidy works (will be overwritten by LLM) @@ -235,10 +248,12 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe prompt := fmt.Sprintf(handlerPrompt, svc.Name, titleName, titleName, proto, strings.Join(epDescs, "\n")) + sp := startSpinner(fmt.Sprintf("writing %s handler...", svc.Name)) resp, err := m.Generate(ctx, &ai.Request{ Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic.", svc.Name), SystemPrompt: prompt, }) + sp.Stop() if err != nil { return err } @@ -274,13 +289,13 @@ func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts i handlerPath := filepath.Join(dir, "handler", name+".go") currentCode := readFile(handlerPath) - fmt.Printf(" \033[33m→\033[0m compile error, fixing (attempt %d/%d)...\n", attempt+1, maxAttempts) - + sp := startSpinner(fmt.Sprintf("fixing compile errors (attempt %d/%d)...", attempt+1, maxAttempts)) resp, fixErr := m.Generate(ctx, &ai.Request{ Prompt: fmt.Sprintf("This Go code has compile errors. Fix ALL of them and return the COMPLETE corrected file.\n\nErrors:\n%s\n\nCode:\n%s", string(out), currentCode), SystemPrompt: "You are a Go expert. Return ONLY the corrected Go code. No markdown, no explanation. Start with 'package handler'.", }) + sp.Stop() if fixErr != nil { return fmt.Errorf("fix attempt failed: %w", fixErr) } @@ -502,6 +517,40 @@ func runIn(dir string, name string, args ...string) error { return cmd.Run() } +type spinner struct { + msg string + stop chan struct{} + done sync.WaitGroup +} + +func startSpinner(msg string) *spinner { + s := &spinner{msg: msg, stop: make(chan struct{})} + s.done.Add(1) + go func() { + defer s.done.Done() + frames := []string{"⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"} + i := 0 + t := time.NewTicker(100 * time.Millisecond) + defer t.Stop() + for { + select { + case <-s.stop: + fmt.Printf("\r\033[K") + return + case <-t.C: + fmt.Printf("\r %s %s", frames[i%len(frames)], msg) + i++ + } + } + }() + return s +} + +func (s *spinner) Stop() { + close(s.stop) + s.done.Wait() +} + func fileHash(path string) string { b, err := os.ReadFile(path) if err != nil { @@ -530,17 +579,25 @@ func writeMeta(svcDir string, m map[string]string) { os.WriteFile(metaPath(svcDir), b, 0644) } -func handlerModified(svcDir, handlerFile string) bool { +func fileModified(svcDir, key, path string) bool { meta := readMeta(svcDir) - savedHash, ok := meta["handler_hash"] + savedHash, ok := meta[key] if !ok { - return false // no hash recorded — treat as unmodified (first run or pre-tracking) + return false } - return fileHash(handlerFile) != savedHash + return fileHash(path) != savedHash } -func recordHandlerHash(svcDir, handlerFile string) { +func recordFileHash(svcDir, key, path string) { meta := readMeta(svcDir) - meta["handler_hash"] = fileHash(handlerFile) + meta[key] = fileHash(path) writeMeta(svcDir, meta) } + +func handlerModified(svcDir, handlerFile string) bool { + return fileModified(svcDir, "handler_hash", handlerFile) +} + +func recordHandlerHash(svcDir, handlerFile string) { + recordFileHash(svcDir, "handler_hash", handlerFile) +} diff --git a/cmd/micro/cli/generate/generate_test.go b/cmd/micro/cli/generate/generate_test.go new file mode 100644 index 0000000000..a3fd3dddf6 --- /dev/null +++ b/cmd/micro/cli/generate/generate_test.go @@ -0,0 +1,343 @@ +package generate + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestToTitle(t *testing.T) { + tests := []struct { + in, want string + }{ + {"order-service", "OrderService"}, + {"task", "Task"}, + {"inventory_item", "InventoryItem"}, + {"hello world", "HelloWorld"}, + {"a-b-c", "ABC"}, + {"already", "Already"}, + } + for _, tt := range tests { + if got := toTitle(tt.in); got != tt.want { + t.Errorf("toTitle(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestProtoType(t *testing.T) { + tests := []struct { + in, want string + }{ + {"string", "string"}, + {"int64", "int64"}, + {"int32", "int32"}, + {"bool", "bool"}, + {"float64", "double"}, + {"unknown", "string"}, + {"", "string"}, + } + for _, tt := range tests { + if got := protoType(tt.in); got != tt.want { + t.Errorf("protoType(%q) = %q, want %q", tt.in, got, tt.want) + } + } +} + +func TestFirstNonEmpty(t *testing.T) { + if got := firstNonEmpty("", "", "c"); got != "c" { + t.Errorf("got %q, want %q", got, "c") + } + if got := firstNonEmpty("a", "b"); got != "a" { + t.Errorf("got %q, want %q", got, "a") + } + if got := firstNonEmpty("", ""); got != "" { + t.Errorf("got %q, want %q", got, "") + } +} + +func TestExtractJSON(t *testing.T) { + tests := []struct { + name, in, want string + }{ + { + "fenced json", + "Here's the design:\n```json\n{\"services\": []}\n```\nDone.", + `{"services": []}`, + }, + { + "fenced no lang", + "```\n{\"a\": 1}\n```", + `{"a": 1}`, + }, + { + "raw json", + `some text {"key": "val"} trailing`, + `{"key": "val"}`, + }, + { + "nested braces", + `{"a": {"b": 1}}`, + `{"a": {"b": 1}}`, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractJSON(tt.in) + if got != tt.want { + t.Errorf("extractJSON() = %q, want %q", got, tt.want) + } + }) + } +} + +func TestExtractCode(t *testing.T) { + tests := []struct { + name, in string + wantPrefix string + }{ + { + "go fence", + "Here:\n```go\npackage handler\n\nfunc Foo() {}\n```\nDone.", + "package handler", + }, + { + "generic fence", + "```\npackage main\n```", + "package main", + }, + { + "raw code", + "Sure, here's the code:\npackage handler\n\ntype X struct{}", + "package handler", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := extractCode(tt.in) + if !strings.HasPrefix(got, tt.wantPrefix) { + t.Errorf("extractCode() = %q, want prefix %q", got, tt.wantPrefix) + } + }) + } +} + +func TestBuildProto(t *testing.T) { + svc := ServiceSpec{ + Name: "task-service", + Description: "Manages tasks", + Fields: []FieldSpec{ + {Name: "id", Type: "string", Description: "Task ID"}, + {Name: "title", Type: "string", Description: "Task title"}, + {Name: "done", Type: "bool", Description: "Completion status"}, + {Name: "created", Type: "int64", Description: "Created timestamp"}, + {Name: "updated", Type: "int64", Description: "Updated timestamp"}, + }, + Endpoints: []EndpointSpec{ + {Name: "Create", Description: "Create a task"}, + {Name: "Read", Description: "Get a task"}, + {Name: "Update", Description: "Update a task"}, + {Name: "Delete", Description: "Delete a task"}, + {Name: "List", Description: "List tasks"}, + {Name: "ToggleComplete", Description: "Toggle completion"}, + }, + } + + proto := buildProto("taskservice", "TaskService", svc) + + checks := []string{ + `syntax = "proto3"`, + `package taskservice`, + `service TaskService`, + `rpc Create(CreateRequest) returns (CreateResponse)`, + `rpc ToggleComplete(ToggleCompleteRequest) returns (ToggleCompleteResponse)`, + `message TaskServiceRecord`, + `string title = 2`, + `bool done = 3`, + `message CreateRequest`, + `message ReadRequest`, + `message DeleteRequest`, + `message ListRequest`, + `message ToggleCompleteRequest`, + } + for _, c := range checks { + if !strings.Contains(proto, c) { + t.Errorf("buildProto() missing %q", c) + } + } + + // Create should not include id, created, updated + createIdx := strings.Index(proto, "message CreateRequest") + createEnd := strings.Index(proto[createIdx:], "}") + createBlock := proto[createIdx : createIdx+createEnd] + for _, skip := range []string{"string id", "int64 created", "int64 updated"} { + if strings.Contains(createBlock, skip) { + t.Errorf("CreateRequest should not contain %q", skip) + } + } +} + +func TestBuildMain(t *testing.T) { + main := buildMain("order-service", "OrderService") + + checks := []string{ + `"order-service/handler"`, + `pb "order-service/proto"`, + `micro.New("orderservice"`, + `pb.RegisterOrderServiceHandler`, + `handler.New()`, + } + for _, c := range checks { + if !strings.Contains(main, c) { + t.Errorf("buildMain() missing %q", c) + } + } +} + +func TestHandlerModifiedTracking(t *testing.T) { + dir := t.TempDir() + handlerDir := filepath.Join(dir, "handler") + os.MkdirAll(handlerDir, 0755) + handlerFile := filepath.Join(handlerDir, "test.go") + + // No .micro file → not modified + os.WriteFile(handlerFile, []byte("package handler\n"), 0644) + if handlerModified(dir, handlerFile) { + t.Error("expected not modified when no .micro exists") + } + + // Record hash → not modified + recordHandlerHash(dir, handlerFile) + if handlerModified(dir, handlerFile) { + t.Error("expected not modified after recording hash") + } + + // Edit the file → modified + os.WriteFile(handlerFile, []byte("package handler\n\nfunc Foo() {}\n"), 0644) + if !handlerModified(dir, handlerFile) { + t.Error("expected modified after editing file") + } + + // Re-record → not modified again + recordHandlerHash(dir, handlerFile) + if handlerModified(dir, handlerFile) { + t.Error("expected not modified after re-recording hash") + } +} + +func TestMetaReadWrite(t *testing.T) { + dir := t.TempDir() + + m := readMeta(dir) + if len(m) != 0 { + t.Error("expected empty meta for new dir") + } + + m["handler_hash"] = "abc123" + m["version"] = "1" + writeMeta(dir, m) + + m2 := readMeta(dir) + if m2["handler_hash"] != "abc123" || m2["version"] != "1" { + t.Errorf("readMeta() = %v, want handler_hash=abc123, version=1", m2) + } +} + +func TestGenerateStructure(t *testing.T) { + dir := t.TempDir() + svcDir := filepath.Join(dir, "test-svc") + + svc := ServiceSpec{ + Name: "test-svc", + Description: "Test service", + Fields: []FieldSpec{ + {Name: "id", Type: "string"}, + {Name: "name", Type: "string"}, + }, + Endpoints: []EndpointSpec{ + {Name: "Create"}, + {Name: "Read"}, + }, + } + + if err := generateStructure(svcDir, svc); err != nil { + t.Fatal(err) + } + + // Check files exist + for _, f := range []string{ + "proto/test-svc.proto", + "handler/test-svc.go", + "main.go", + "go.mod", + "Makefile", + ".gitignore", + } { + if _, err := os.Stat(filepath.Join(svcDir, f)); err != nil { + t.Errorf("missing %s: %v", f, err) + } + } + + // Check .micro was created with handler hash + meta := readMeta(svcDir) + if meta["handler_hash"] == "" { + t.Error("expected handler_hash in .micro after generateStructure") + } + + // Run again — should not overwrite main.go + mainBefore, _ := os.ReadFile(filepath.Join(svcDir, "main.go")) + os.WriteFile(filepath.Join(svcDir, "main.go"), []byte("// user edited\n"), 0644) + if err := generateStructure(svcDir, svc); err != nil { + t.Fatal(err) + } + mainAfter, _ := os.ReadFile(filepath.Join(svcDir, "main.go")) + if string(mainAfter) == string(mainBefore) { + t.Error("expected main.go to keep user edit on re-run") + } + + // Proto should be protected if user modified it + protoFile := filepath.Join(svcDir, "proto", "test-svc.proto") + protoBefore, _ := os.ReadFile(protoFile) + os.WriteFile(protoFile, []byte("// user-edited proto\n"), 0644) + if err := generateStructure(svcDir, svc); err != nil { + t.Fatal(err) + } + protoAfter, _ := os.ReadFile(protoFile) + if string(protoAfter) != "// user-edited proto\n" { + t.Error("expected proto to be preserved after user edit") + } + + // Proto should regenerate if NOT modified + recordFileHash(svcDir, "proto_hash", protoFile) + if err := generateStructure(svcDir, svc); err != nil { + t.Fatal(err) + } + protoAfter2, _ := os.ReadFile(protoFile) + if string(protoAfter2) == string(protoBefore) { + // ok — regenerated from spec + } +} + +func TestFileModified(t *testing.T) { + dir := t.TempDir() + f := filepath.Join(dir, "test.txt") + os.WriteFile(f, []byte("original"), 0644) + + // No hash → not modified + if fileModified(dir, "test_hash", f) { + t.Error("expected not modified with no saved hash") + } + + recordFileHash(dir, "test_hash", f) + + // Same content → not modified + if fileModified(dir, "test_hash", f) { + t.Error("expected not modified with matching hash") + } + + // Changed content → modified + os.WriteFile(f, []byte("changed"), 0644) + if !fileModified(dir, "test_hash", f) { + t.Error("expected modified after content change") + } +} From 6ee76126dab1abee8f8e619879395f3228e04603 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:09:07 +0000 Subject: [PATCH 06/19] feat: signal handling, existing service discovery, help text - Ctrl+C during generation now cancels LLM calls immediately via signal-aware context; re-run picks up where it left off - Design() scans for existing services in the working directory and includes their proto definitions in the prompt, so the LLM extends the system rather than redesigning from scratch - Updated --prompt help text with usage examples on both new and run - Listed all supported providers in flag descriptions - Added discoverExisting test https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/cli.go | 11 +-- cmd/micro/cli/generate/generate.go | 91 ++++++++++++++++++++++++- cmd/micro/cli/generate/generate_test.go | 40 +++++++++++ cmd/micro/cli/new/new.go | 15 ++-- cmd/micro/run/run.go | 19 ++++-- 5 files changed, 158 insertions(+), 18 deletions(-) diff --git a/cmd/micro/cli/cli.go b/cmd/micro/cli/cli.go index 7d62be2c2b..2610194a5b 100644 --- a/cmd/micro/cli/cli.go +++ b/cmd/micro/cli/cli.go @@ -42,7 +42,10 @@ func init() { Name: "new", Usage: "Create a new service", ArgsUsage: "[name]", - Action: new.Run, + UsageText: ` micro new helloworld # scaffold a single service + micro new --prompt "a todo list with tasks" # AI-design multiple services + micro new --prompt "add tags to the task service" # extend existing services`, + Action: new.Run, Flags: []cli.Flag{ &cli.BoolFlag{ Name: "no-mcp", @@ -54,17 +57,17 @@ func init() { }, &cli.StringFlag{ Name: "prompt", - Usage: "Describe the system to generate (uses AI to design services)", + Usage: "Describe the system to generate (uses AI to design & build services with real business logic)", EnvVars: []string{"MICRO_NEW_PROMPT"}, }, &cli.StringFlag{ Name: "provider", - Usage: "AI provider for --prompt (anthropic, openai, gemini, etc.)", + Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)", EnvVars: []string{"MICRO_AI_PROVIDER"}, }, &cli.StringFlag{ Name: "api_key", - Usage: "API key for --prompt", + Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)", EnvVars: []string{"MICRO_AI_API_KEY"}, }, }, diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index f94cad588a..e71490577a 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -56,6 +56,42 @@ Rules: - Examples should be realistic JSON - 2-5 services max, focused on the domain` +const designPromptWithExisting = `You are a Go microservices architect using the go-micro framework. +The user has an EXISTING system with services already running. They want to extend or modify it. + +Existing services: +%s + +Given the user's request, return the COMPLETE set of services (existing + new/modified). +For existing services the user hasn't asked to change, return them as-is. +For new or modified services, include the full specification. + +Return ONLY valid JSON: +{ + "services": [ + { + "name": "service-name", + "description": "What this service does", + "fields": [ + {"name": "field_name", "type": "string", "description": "What this field is"} + ], + "endpoints": [ + {"name": "EndpointName", "description": "What this endpoint does", "example": "{\"key\": \"value\"}"} + ] + } + ] +} + +Rules: +- Service names are lowercase, hyphenated +- Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List +- Add custom endpoints for real business logic +- Field types: string, int64, bool, float64 +- Every service needs id (string), created (int64), updated (int64) fields +- Endpoint names are PascalCase +- Examples should be realistic JSON +- Keep existing services unless the user explicitly asks to change them` + const handlerPrompt = `You are a Go developer writing a handler for a go-micro service. Generate a COMPLETE, COMPILABLE Go handler file. @@ -106,16 +142,29 @@ type EndpointSpec struct { } // Design calls an LLM to design services from a prompt. -func Design(ctx context.Context, provider, apiKey, model, prompt string) (*ServiceDesign, error) { +// If baseDir contains existing services, they are included as context +// so the LLM extends the system rather than redesigning from scratch. +func Design(ctx context.Context, provider, apiKey, model, baseDir, prompt string) (*ServiceDesign, error) { m := newModel(provider, apiKey, model) if m == nil { return nil, fmt.Errorf("unknown provider: %s", provider) } + existing := discoverExisting(baseDir) + + var sysPrompt, userPrompt string + if len(existing) > 0 { + sysPrompt = fmt.Sprintf(designPromptWithExisting, existing) + userPrompt = fmt.Sprintf("Extend or modify the system: %s", prompt) + } else { + sysPrompt = designPrompt + userPrompt = fmt.Sprintf("Design a microservices system for: %s", prompt) + } + sp := startSpinner("designing services...") resp, err := m.Generate(ctx, &ai.Request{ - Prompt: fmt.Sprintf("Design a microservices system for: %s", prompt), - SystemPrompt: designPrompt, + Prompt: userPrompt, + SystemPrompt: sysPrompt, }) sp.Stop() if err != nil { @@ -135,6 +184,39 @@ func Design(ctx context.Context, provider, apiKey, model, prompt string) (*Servi return &design, nil } +// discoverExisting scans a directory for existing go-micro services +// and returns a summary string for inclusion in the design prompt. +func discoverExisting(baseDir string) string { + entries, err := os.ReadDir(baseDir) + if err != nil { + return "" + } + + var summaries []string + for _, e := range entries { + if !e.IsDir() { + continue + } + svcDir := filepath.Join(baseDir, e.Name()) + + // Look for proto files as indicator of a go-micro service + protoDir := filepath.Join(svcDir, "proto") + protos, err := filepath.Glob(filepath.Join(protoDir, "*.proto")) + if err != nil || len(protos) == 0 { + continue + } + + proto := readFile(protos[0]) + if proto == "" { + continue + } + + summaries = append(summaries, fmt.Sprintf("### %s\nProto:\n```\n%s\n```", e.Name(), proto)) + } + + return strings.Join(summaries, "\n\n") +} + // Generate creates go-micro service directories from a design. // If a service directory already exists, it skips structure generation // but regenerates the handler (allowing iterative improvement). @@ -142,6 +224,9 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid m := newModel(provider, apiKey, model) for i, svc := range design.Services { + if ctx.Err() != nil { + return ctx.Err() + } svcDir := filepath.Join(baseDir, svc.Name) fmt.Printf(" \033[2m[%d/%d]\033[0m generating \033[36m%s\033[0m...\n", i+1, len(design.Services), svc.Name) diff --git a/cmd/micro/cli/generate/generate_test.go b/cmd/micro/cli/generate/generate_test.go index a3fd3dddf6..2daeb6f6a8 100644 --- a/cmd/micro/cli/generate/generate_test.go +++ b/cmd/micro/cli/generate/generate_test.go @@ -341,3 +341,43 @@ func TestFileModified(t *testing.T) { t.Error("expected modified after content change") } } + +func TestDiscoverExisting(t *testing.T) { + dir := t.TempDir() + + // Empty directory → empty string + if got := discoverExisting(dir); got != "" { + t.Errorf("expected empty for empty dir, got %q", got) + } + + // Non-service directory (no proto) → empty + os.MkdirAll(filepath.Join(dir, "not-a-service"), 0755) + if got := discoverExisting(dir); got != "" { + t.Errorf("expected empty for dir without proto, got %q", got) + } + + // Create a real service directory with proto + svcDir := filepath.Join(dir, "order-service") + os.MkdirAll(filepath.Join(svcDir, "proto"), 0755) + os.WriteFile(filepath.Join(svcDir, "proto", "order-service.proto"), + []byte("syntax = \"proto3\";\nservice OrderService {}"), 0644) + + got := discoverExisting(dir) + if !strings.Contains(got, "order-service") { + t.Errorf("expected to find order-service, got %q", got) + } + if !strings.Contains(got, "OrderService") { + t.Errorf("expected to find proto content, got %q", got) + } + + // Add a second service + svc2Dir := filepath.Join(dir, "user-service") + os.MkdirAll(filepath.Join(svc2Dir, "proto"), 0755) + os.WriteFile(filepath.Join(svc2Dir, "proto", "user-service.proto"), + []byte("syntax = \"proto3\";\nservice UserService {}"), 0644) + + got = discoverExisting(dir) + if !strings.Contains(got, "order-service") || !strings.Contains(got, "user-service") { + t.Errorf("expected both services, got %q", got) + } +} diff --git a/cmd/micro/cli/new/new.go b/cmd/micro/cli/new/new.go index ae4e4d36f3..5cb76e2951 100644 --- a/cmd/micro/cli/new/new.go +++ b/cmd/micro/cli/new/new.go @@ -7,10 +7,12 @@ import ( "go/build" "os" "os/exec" + "os/signal" "path" "path/filepath" "runtime" "strings" + "syscall" "go-micro.dev/v5/cmd/micro/cli/generate" "text/template" @@ -316,9 +318,9 @@ func printTree(dir string) { fmt.Println(t.String()) } -func runPrompt(ctx *cli.Context, prompt string) error { - provider := ctx.String("provider") - apiKey := ctx.String("api_key") +func runPrompt(cliCtx *cli.Context, prompt string) error { + provider := cliCtx.String("provider") + apiKey := cliCtx.String("api_key") if apiKey == "" { // Try provider-specific env vars for _, env := range []string{"ANTHROPIC_API_KEY", "OPENAI_API_KEY", "GEMINI_API_KEY", @@ -333,12 +335,15 @@ func runPrompt(ctx *cli.Context, prompt string) error { return fmt.Errorf("--api_key or a provider API key env var is required for --prompt") } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + fmt.Println() fmt.Println(" \033[1mmicro new --prompt\033[0m") fmt.Println() fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt) - design, err := generate.Design(context.Background(), provider, apiKey, "", prompt) + design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt) if err != nil { return fmt.Errorf("design failed: %w", err) } @@ -353,7 +358,7 @@ func runPrompt(ctx *cli.Context, prompt string) error { fmt.Println() fmt.Println(" Generating code...") - if err := generate.Generate(context.Background(), ".", design, provider, apiKey, ""); err != nil { + if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) } diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index 25e70fcd1a..557fccbe34 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -498,7 +498,8 @@ Examples: micro run --no-gateway # Services only, no HTTP gateway micro run --no-watch # Disable hot reload micro run --env production # Use production environment - micro run --mcp-address :3000 # Enable MCP protocol gateway`, + micro run --mcp-address :3000 # Enable MCP protocol gateway + micro run --prompt "an order system for dropshipping" # Generate and run`, Action: Run, Flags: []cli.Flag{ &cli.StringFlag{ @@ -528,17 +529,17 @@ Examples: }, &cli.StringFlag{ Name: "prompt", - Usage: "Describe a system to generate and run (uses AI to design services)", + Usage: "Describe a system to generate and run (AI designs, builds, and starts services)", EnvVars: []string{"MICRO_RUN_PROMPT"}, }, &cli.StringFlag{ Name: "provider", - Usage: "AI provider for --prompt", + Usage: "AI provider for --prompt (anthropic, openai, gemini, atlascloud, groq, mistral, together)", EnvVars: []string{"MICRO_AI_PROVIDER"}, }, &cli.StringFlag{ Name: "api_key", - Usage: "API key for --prompt", + Usage: "API key for --prompt (or set ANTHROPIC_API_KEY, OPENAI_API_KEY, etc.)", EnvVars: []string{"MICRO_AI_API_KEY"}, }, }, @@ -561,12 +562,15 @@ func runWithPrompt(c *cli.Context, prompt string) error { return fmt.Errorf("--api_key or a provider API key env var is required for --prompt") } + ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + fmt.Println() fmt.Println(" \033[1mmicro run --prompt\033[0m") fmt.Println() fmt.Printf(" \033[2mDesigning services for:\033[0m %s\n\n", prompt) - design, err := generate.Design(context.Background(), provider, apiKey, "", prompt) + design, err := generate.Design(ctx, provider, apiKey, "", ".", prompt) if err != nil { return fmt.Errorf("design failed: %w", err) } @@ -578,7 +582,7 @@ func runWithPrompt(c *cli.Context, prompt string) error { fmt.Println() fmt.Println(" Generating code...") - if err := generate.Generate(context.Background(), ".", design, provider, apiKey, ""); err != nil { + if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) } for _, svc := range design.Services { @@ -590,6 +594,9 @@ func runWithPrompt(c *cli.Context, prompt string) error { fmt.Println(" Starting services...") fmt.Println() + // Cancel signal context before handing off to Run (which manages its own signals) + cancel() + // Run normally from current directory (services are now generated) // Set the prompt to empty via a new context so Run doesn't recurse c.Set("prompt", "") From 807cc741e8ee8884718fb402bc99153cd8d260c7 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 15:30:39 +0000 Subject: [PATCH 07/19] feat: show endpoints in run --prompt output, add micro chat hint Print endpoint names and descriptions when designing services so users see what was built. Add a micro chat hint to the run banner so users know how to interact with their services after startup. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/run/run.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index 557fccbe34..78dd1931e7 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -472,6 +472,9 @@ func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, fmt.Println(" \033[33mWatching for changes...\033[0m") } + fmt.Println() + fmt.Println(" \033[2mmicro chat --provider anthropic # talk to your services\033[0m") + fmt.Println() } @@ -578,6 +581,9 @@ func runWithPrompt(c *cli.Context, prompt string) error { fmt.Println(" Services:") for _, svc := range design.Services { fmt.Printf(" \033[32m●\033[0m \033[36m%s\033[0m — %s\n", svc.Name, svc.Description) + for _, ep := range svc.Endpoints { + fmt.Printf(" %s: %s\n", ep.Name, ep.Description) + } } fmt.Println() From c0936f56f2b5afca05ab05d1852408d994ffb015 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 16:09:51 +0000 Subject: [PATCH 08/19] fix: generated go.mod uses go 1.24 with explicit go-micro require go 1.22 with no explicit require caused Go to resolve sub-packages (gateway/mcp, client, server) as separate modules, hitting stale v1.18 tags. Pin to go 1.24 + require go-micro.dev/v5 v5.24.0 so go mod tidy resolves all sub-packages from the root module correctly. Tested end-to-end: 4 services generated and compiled successfully. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/generate/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index e71490577a..d5139a2d40 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -290,7 +290,7 @@ func generateStructure(dir string, svc ServiceSpec) error { "GOPATH:=$(shell go env GOPATH)\n\n.PHONY: proto\nproto:\n\tprotoc --proto_path=. --micro_out=. --go_out=. proto/*.proto\n") writeFile(filepath.Join(dir, "go.mod"), - fmt.Sprintf("module %s\n\ngo 1.22\n", name)) + fmt.Sprintf("module %s\n\ngo 1.24\n\nrequire go-micro.dev/v5 v5.24.0\n", name)) writeFile(filepath.Join(dir, ".gitignore"), fmt.Sprintf("%s\n.micro\n", name)) From ac332cd6d98cd611b2d1081dfe89f63c7083ab93 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 16:14:09 +0000 Subject: [PATCH 09/19] fix: skip handler regeneration when proto unchanged Compare proto hash before and after structure generation. If the proto didn't change and the handler wasn't edited by the user, skip go mod tidy, make proto, LLM handler generation, and compile-fix entirely. Prints "(unchanged)" instead. Reduces re-run of 4-service project from ~2 minutes to ~10 seconds. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/generate/generate.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index d5139a2d40..6697e80e62 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -228,6 +228,12 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid return ctx.Err() } svcDir := filepath.Join(baseDir, svc.Name) + handlerFile := filepath.Join(svcDir, "handler", svc.Name+".go") + protoFile := filepath.Join(svcDir, "proto", svc.Name+".proto") + + // Snapshot proto hash before structure generation + protoBefore := fileHash(protoFile) + fmt.Printf(" \033[2m[%d/%d]\033[0m generating \033[36m%s\033[0m...\n", i+1, len(design.Services), svc.Name) // Step 1: Generate proto (deterministic — from design spec) @@ -235,12 +241,21 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid return fmt.Errorf("structure %s: %w", svc.Name, err) } + protoAfter := fileHash(protoFile) + protoChanged := protoBefore != protoAfter + + // If proto unchanged and handler unmodified, nothing to do + if !protoChanged && protoBefore != "" && !handlerModified(svcDir, handlerFile) { + fmt.Printf(" \033[32m✓\033[0m %s \033[2m(unchanged)\033[0m\n", svc.Name) + continue + } + // Step 2: Run go mod tidy + make proto to get compiled proto runIn(svcDir, "go", "mod", "tidy") runIn(svcDir, "make", "proto") // Step 3: Generate handler with business logic (LLM) - proto := readFile(filepath.Join(svcDir, "proto", svc.Name+".proto")) + proto := readFile(protoFile) if err := generateHandler(ctx, m, svcDir, svc, proto); err != nil { return fmt.Errorf("handler %s: %w", svc.Name, err) } @@ -253,7 +268,7 @@ func Generate(ctx context.Context, baseDir string, design *ServiceDesign, provid } // Record final handler hash (after any compile fixes) - recordHandlerHash(svcDir, filepath.Join(svcDir, "handler", svc.Name+".go")) + recordHandlerHash(svcDir, handlerFile) } return nil } From 956000e191cd6979653ca5d0a974b7c21cceb7f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 17:25:44 +0000 Subject: [PATCH 10/19] feat: confirm design before generating code Show the service design (names, endpoints) and prompt "Generate? [Y/n]" before spending LLM time on handler generation. Applies to both micro new --prompt and micro run --prompt. Default is yes (enter). https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/new/new.go | 16 ++++++++++++++++ cmd/micro/run/run.go | 15 +++++++++++++++ 2 files changed, 31 insertions(+) diff --git a/cmd/micro/cli/new/new.go b/cmd/micro/cli/new/new.go index 5cb76e2951..9c2ef3309f 100644 --- a/cmd/micro/cli/new/new.go +++ b/cmd/micro/cli/new/new.go @@ -2,6 +2,7 @@ package new import ( + "bufio" "context" "fmt" "go/build" @@ -357,6 +358,11 @@ func runPrompt(cliCtx *cli.Context, prompt string) error { } fmt.Println() + if !confirmGenerate() { + fmt.Println(" Cancelled.") + return nil + } + fmt.Println(" Generating code...") if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) @@ -375,3 +381,13 @@ func runPrompt(cliCtx *cli.Context, prompt string) error { fmt.Println() return nil } + +func confirmGenerate() bool { + fmt.Print(" Generate? [Y/n] ") + scanner := bufio.NewScanner(os.Stdin) + if !scanner.Scan() { + return false + } + answer := strings.TrimSpace(strings.ToLower(scanner.Text())) + return answer == "" || answer == "y" || answer == "yes" +} diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index 78dd1931e7..65eda17210 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -587,6 +587,11 @@ func runWithPrompt(c *cli.Context, prompt string) error { } fmt.Println() + if !confirmGenerate() { + fmt.Println(" Cancelled.") + return nil + } + fmt.Println(" Generating code...") if err := generate.Generate(ctx, ".", design, provider, apiKey, ""); err != nil { return fmt.Errorf("generate failed: %w", err) @@ -608,3 +613,13 @@ func runWithPrompt(c *cli.Context, prompt string) error { c.Set("prompt", "") return Run(c) } + +func confirmGenerate() bool { + fmt.Print(" Generate? [Y/n] ") + scanner := bufio.NewScanner(os.Stdin) + if !scanner.Scan() { + return false + } + answer := strings.TrimSpace(strings.ToLower(scanner.Text())) + return answer == "" || answer == "y" || answer == "yes" +} From 850837ff072bc0467903ab0dbdcabfd962f3f4f5 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 17:26:17 +0000 Subject: [PATCH 11/19] fix: use port :0 for MCP in generated multi-service projects Each generated service had mcp.WithMCP(":3001") hardcoded, causing port conflicts when running multiple services. Use :0 to auto-assign a free port. micro run's central gateway handles unified MCP access. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/cli/generate/generate.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 6697e80e62..5e1eae0658 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -506,7 +506,7 @@ import ( func main() { service := micro.New("%s", - mcp.WithMCP(":3001"), + mcp.WithMCP(":0"), ) service.Init() pb.Register%sHandler(service.Server(), handler.New()) From f82047e38ab2d87c8d76cdb2f25dc5f52e3c5bb3 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:28:38 +0000 Subject: [PATCH 12/19] feat: truncation detection, tool result display in chat MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Detect truncated LLM responses (unbalanced braces, doesn't end with '}') and retry with a conciseness hint before falling through to compile-fix - Show tool call results in micro chat output (← for success, ✗ for errors) so users can see what the LLM did - Add Result/Error fields to ToolCall, populated by Anthropic provider after tool execution - Add isTruncated tests Tested end-to-end with Anthropic: services generate, compile, start, register, respond to RPC calls, and micro chat discovers and calls tools correctly. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- ai/anthropic/anthropic.go | 3 +- ai/model.go | 10 +++--- cmd/micro/chat/chat.go | 13 ++++++++ cmd/micro/cli/generate/generate.go | 43 ++++++++++++++++++++++++- cmd/micro/cli/generate/generate_test.go | 23 +++++++++++++ 5 files changed, 86 insertions(+), 6 deletions(-) diff --git a/ai/anthropic/anthropic.go b/ai/anthropic/anthropic.go index f2ffdffc6f..203510f156 100644 --- a/ai/anthropic/anthropic.go +++ b/ai/anthropic/anthropic.go @@ -102,8 +102,9 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen // If tool handler is provided, execute tools and get final answer if p.opts.ToolHandler != nil { var toolResults []ai.ToolResult - for _, tc := range resp.ToolCalls { + for i, tc := range resp.ToolCalls { _, content := p.opts.ToolHandler(tc.Name, tc.Input) + resp.ToolCalls[i].Result = content toolResults = append(toolResults, ai.ToolResult{ ID: tc.ID, Content: content, diff --git a/ai/model.go b/ai/model.go index d8d3c4b573..4ae403ef52 100644 --- a/ai/model.go +++ b/ai/model.go @@ -57,11 +57,13 @@ type Response struct { Answer string } -// ToolCall represents a request to call a tool +// ToolCall represents a request to call a tool and its result type ToolCall struct { - ID string // Tool call ID (for correlation) - Name string // Tool name - Input map[string]any // Tool input arguments + ID string // Tool call ID (for correlation) + Name string // Tool name + Input map[string]any // Tool input arguments + Result string // Tool execution result (populated after execution) + Error string // Tool execution error (populated after execution) } // ToolResult represents the result of a tool execution diff --git a/cmd/micro/chat/chat.go b/cmd/micro/chat/chat.go index 228553c0ce..954a9185a3 100644 --- a/cmd/micro/chat/chat.go +++ b/cmd/micro/chat/chat.go @@ -195,6 +195,12 @@ func ask(ctx context.Context, m ai.Model, hist *ai.History, toolList []ai.Tool, for _, tc := range resp.ToolCalls { args, _ := json.Marshal(tc.Input) fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args) + if tc.Result != "" { + fmt.Printf(" \033[32m←\033[0m \033[2m%s\033[0m\n", truncateResult(tc.Result)) + } + if tc.Error != "" { + fmt.Printf(" \033[31m✗\033[0m %s\n", tc.Error) + } } if resp.Answer != "" { fmt.Println() @@ -234,3 +240,10 @@ func envVarForProvider(provider string) string { return "MICRO_AI_API_KEY" } } + +func truncateResult(s string) string { + if len(s) <= 200 { + return s + } + return s[:200] + "..." +} diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 5e1eae0658..29035ef765 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -365,6 +365,25 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe return fmt.Errorf("LLM did not return valid Go code") } + if isTruncated(code) { + fmt.Printf(" \033[33m→\033[0m response truncated, retrying...\n") + sp = startSpinner(fmt.Sprintf("rewriting %s handler...", svc.Name)) + resp, err = m.Generate(ctx, &ai.Request{ + Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic. Keep it concise — no more than 300 lines.", svc.Name), + SystemPrompt: prompt, + }) + sp.Stop() + if err != nil { + return err + } + code = firstNonEmpty(resp.Answer, resp.Reply) + code = extractCode(code) + } + + if !strings.HasPrefix(strings.TrimSpace(code), "package") { + return fmt.Errorf("LLM did not return valid Go code") + } + writeFile(handlerFile, code) recordHandlerHash(dir, handlerFile) return nil @@ -402,7 +421,7 @@ func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts i fixed := firstNonEmpty(resp.Answer, resp.Reply) fixed = extractCode(fixed) - if strings.HasPrefix(strings.TrimSpace(fixed), "package") { + if strings.HasPrefix(strings.TrimSpace(fixed), "package") && !isTruncated(fixed) { writeFile(handlerPath, fixed) } } @@ -565,6 +584,28 @@ func extractCode(s string) string { return strings.TrimSpace(s) } +func isTruncated(code string) bool { + trimmed := strings.TrimSpace(code) + if len(trimmed) == 0 { + return true + } + // Valid Go files end with a closing brace + if trimmed[len(trimmed)-1] != '}' { + return true + } + // Check balanced braces + depth := 0 + for _, c := range trimmed { + switch c { + case '{': + depth++ + case '}': + depth-- + } + } + return depth != 0 +} + func protoType(t string) string { switch t { case "int64": diff --git a/cmd/micro/cli/generate/generate_test.go b/cmd/micro/cli/generate/generate_test.go index 2daeb6f6a8..81017948f1 100644 --- a/cmd/micro/cli/generate/generate_test.go +++ b/cmd/micro/cli/generate/generate_test.go @@ -381,3 +381,26 @@ func TestDiscoverExisting(t *testing.T) { t.Errorf("expected both services, got %q", got) } } + +func TestIsTruncated(t *testing.T) { + tests := []struct { + name string + code string + want bool + }{ + {"complete", "package handler\n\nfunc New() *H { return &H{} }\n", false}, + {"empty", "", true}, + {"no closing brace", "package handler\n\nfunc Foo() {", true}, + {"unbalanced", "package handler\n\nfunc Foo() {\n\tif true {", true}, + {"balanced", "package handler\n\nfunc Foo() {\n\tif true {\n\t}\n}", false}, + {"trailing whitespace ok", "package handler\n\ntype X struct{}\n\n", false}, + {"mid-expression", "package handler\n\nfunc F() {\n\tx := 1 +", true}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := isTruncated(tt.code); got != tt.want { + t.Errorf("isTruncated() = %v, want %v", got, tt.want) + } + }) + } +} From ce50873fc34f7e5bbf6a57acdf1cbf534ff3a9bb Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:47:14 +0000 Subject: [PATCH 13/19] fix: Anthropic tool loop, service naming, chat tool results MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Anthropic provider: - Fix tool execution loop to properly iterate (was re-processing all tool calls instead of only new ones each round) - Clean assistant content blocks before sending back (strip 'id' from text blocks that Anthropic rejects on input) - Include tools in follow-up requests so model can make additional calls - Loop up to 10 rounds until model responds with text only Service naming: - Strip '-service' suffix from micro.New() name so services register as 'task', 'category' instead of 'taskservice', 'categoryservice' Chat: - Show tool results (← for success) and errors (✗) in chat output Tested end-to-end: create task → list tasks works as multi-step orchestration through micro chat with Anthropic Claude. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- ai/anthropic/anthropic.go | 94 ++++++++++++++++++------- cmd/micro/cli/generate/generate.go | 3 +- cmd/micro/cli/generate/generate_test.go | 2 +- 3 files changed, 72 insertions(+), 27 deletions(-) diff --git a/ai/anthropic/anthropic.go b/ai/anthropic/anthropic.go index 203510f156..5f2b5d0896 100644 --- a/ai/anthropic/anthropic.go +++ b/ai/anthropic/anthropic.go @@ -94,49 +94,66 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen return nil, err } - // If no tool calls, return response - if len(resp.ToolCalls) == 0 { + // If no tool calls or no handler, return as-is + if len(resp.ToolCalls) == 0 || p.opts.ToolHandler == nil { return resp, nil } - // If tool handler is provided, execute tools and get final answer - if p.opts.ToolHandler != nil { - var toolResults []ai.ToolResult - for i, tc := range resp.ToolCalls { - _, content := p.opts.ToolHandler(tc.Name, tc.Input) - resp.ToolCalls[i].Result = content - toolResults = append(toolResults, ai.ToolResult{ - ID: tc.ID, - Content: content, - }) - } + // Tool execution loop: execute tools, send results back, repeat + // until the model responds with text only (no more tool calls) + messages := []map[string]any{ + {"role": "user", "content": req.Prompt}, + {"role": "assistant", "content": cleanContent(rawContent)}, + } - // Build follow-up request with tool results + pendingCalls := resp.ToolCalls + + for rounds := 0; rounds < 10; rounds++ { var toolResultBlocks []map[string]any - for _, tr := range toolResults { + for i := range pendingCalls { + _, content := p.opts.ToolHandler(pendingCalls[i].Name, pendingCalls[i].Input) + pendingCalls[i].Result = content toolResultBlocks = append(toolResultBlocks, map[string]any{ "type": "tool_result", - "tool_use_id": tr.ID, - "content": tr.Content, + "tool_use_id": pendingCalls[i].ID, + "content": content, }) } + messages = append(messages, map[string]any{ + "role": "user", + "content": toolResultBlocks, + }) + followUpReq := map[string]any{ "model": p.opts.Model, "max_tokens": 4096, "system": req.SystemPrompt, - "messages": []map[string]any{ - {"role": "user", "content": req.Prompt}, - {"role": "assistant", "content": rawContent}, - {"role": "user", "content": toolResultBlocks}, - }, + "messages": messages, + } + if len(anthropicTools) > 0 { + followUpReq["tools"] = anthropicTools + } + + followUpResp, followUpRaw, err := p.callAPI(ctx, followUpReq) + if err != nil { + break } - // Make follow-up API call - followUpResp, _, err := p.callAPI(ctx, followUpReq) - if err == nil && followUpResp.Reply != "" { + if len(followUpResp.ToolCalls) > 0 { + resp.ToolCalls = append(resp.ToolCalls, followUpResp.ToolCalls...) + pendingCalls = followUpResp.ToolCalls + messages = append(messages, map[string]any{ + "role": "assistant", + "content": cleanContent(followUpRaw), + }) + continue + } + + if followUpResp.Reply != "" { resp.Answer = followUpResp.Reply } + break } return resp, nil @@ -226,3 +243,30 @@ func (p *Provider) callAPI(ctx context.Context, req map[string]any) (*ai.Respons return response, anthropicResp.Content, nil } + +// cleanContent strips fields from response content blocks that Anthropic +// rejects when sent back as assistant message content (e.g. "id" on text blocks). +func cleanContent(raw any) any { + blocks, ok := raw.([]struct { + Type string `json:"type"` + Text string `json:"text"` + ID string `json:"id"` + Name string `json:"name"` + Input json.RawMessage `json:"input"` + }) + if !ok { + return raw + } + var cleaned []map[string]any + for _, b := range blocks { + switch b.Type { + case "text": + cleaned = append(cleaned, map[string]any{"type": "text", "text": b.Text}) + case "tool_use": + var input any + json.Unmarshal(b.Input, &input) + cleaned = append(cleaned, map[string]any{"type": "tool_use", "id": b.ID, "name": b.Name, "input": input}) + } + } + return cleaned +} diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 29035ef765..01129c9565 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -513,6 +513,7 @@ func buildProto(dehyphen, titleName string, svc ServiceSpec) string { } func buildMain(name, titleName string) string { + svcName := strings.TrimSuffix(name, "-service") return fmt.Sprintf(`package main import ( @@ -531,7 +532,7 @@ func main() { pb.Register%sHandler(service.Server(), handler.New()) service.Run() } -`, name, name, strings.ReplaceAll(name, "-", ""), titleName) +`, name, name, svcName, titleName) } func extractJSON(s string) string { diff --git a/cmd/micro/cli/generate/generate_test.go b/cmd/micro/cli/generate/generate_test.go index 81017948f1..86af3f25c6 100644 --- a/cmd/micro/cli/generate/generate_test.go +++ b/cmd/micro/cli/generate/generate_test.go @@ -183,7 +183,7 @@ func TestBuildMain(t *testing.T) { checks := []string{ `"order-service/handler"`, `pb "order-service/proto"`, - `micro.New("orderservice"`, + `micro.New("order"`, `pb.RegisterOrderServiceHandler`, `handler.New()`, } From cc87e00d87b7d3ef735139369b710721bf094c74 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:51:17 +0000 Subject: [PATCH 14/19] =?UTF-8?q?feat:=20blog=20post=2013=20=E2=80=94=20fr?= =?UTF-8?q?om=20prompt=20to=20production?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Covers the full micro run --prompt flow: design, generate, compile-fix, run, and chat orchestration. Positions agent-as-orchestrator as the answer to service coordination. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- internal/website/blog/13.md | 144 ++++++++++++++++++++++++++++++++++++ 1 file changed, 144 insertions(+) create mode 100644 internal/website/blog/13.md diff --git a/internal/website/blog/13.md b/internal/website/blog/13.md new file mode 100644 index 0000000000..348d659538 --- /dev/null +++ b/internal/website/blog/13.md @@ -0,0 +1,144 @@ +--- +layout: blog +title: "From Prompt to Production: AI-Generated Microservices That Actually Run" +permalink: /blog/13 +description: "micro run --prompt generates real services with business logic, compiles them, starts them, and lets you talk to them. Here's how it works and why it matters." +--- + +# From Prompt to Production: AI-Generated Microservices That Actually Run + +*June 3, 2026 • By the Go Micro Team* + +Every code generator promises the same thing: describe what you want, get code out. The reality is always a skeleton — empty handlers, TODO comments, and hours of work before anything actually runs. + +We built something different. One command generates real microservices with business logic, compiles them, starts them, and lets you talk to them through an AI agent. No intermediate steps. + +``` +micro run --prompt "a todo list with tasks and categories" +``` + +That's it. Two services start, register, and respond to requests. An AI agent can create tasks, list categories, and orchestrate across services immediately. + +## What Actually Happens + +The flow has four stages, each solving a problem that previous generators ignore. + +**1. Design** — an LLM architects the system from your description. It returns a structured JSON spec with service names, fields, endpoints, and example inputs. You see the design and confirm before anything is generated. + +``` +Services: + ● category — Manages task categories + CreateCategory, ReadCategory, ListCategories, ... + ● task — Core task management + CreateTask, CompleteTask, GetOverdueTasks, ... + +Generate? [Y/n] +``` + +**2. Generate** — for each service, proto files are written deterministically from the spec. Then the LLM writes the handler with real business logic: input validation, edge cases, thread-safe in-memory storage, doc comments, and `@example` tags that become MCP tool descriptions. + +Here's what the generated `CompleteTask` handler looks like — not a TODO, not a stub: + +```go +func (s *TaskService) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, + rsp *pb.CompleteTaskResponse) error { + if req.Id == "" { + return errors.New("id is required") + } + s.mu.Lock() + defer s.mu.Unlock() + task, exists := s.tasks[req.Id] + if !exists { + return errors.New("task not found") + } + if task.Completed { + rsp.Success = false + rsp.Message = "task already completed" + return nil + } + task.Completed = true + task.Updated = time.Now().Unix() + rsp.Record = task + rsp.Success = true + rsp.Message = "task marked as completed" + return nil +} +``` + +**3. Compile-fix** — every generated handler is compiled immediately. If it fails, the errors are fed back to the LLM for correction, up to three iterations. If the response was truncated (unbalanced braces), it retries with a conciseness hint. Most services compile on the first attempt. + +**4. Run** — `micro run` builds each service, starts them as separate processes, registers them via mDNS, and launches an HTTP gateway. Every endpoint is immediately accessible via REST, gRPC, or MCP. + +## The Agent Layer + +The real insight isn't code generation. It's what happens after the services start. + +``` +micro chat --provider anthropic +> Create a Work category, then add a task called 'Finish report' to it +``` + +The agent discovers both services from the registry, sees every endpoint as a tool, and orchestrates: + +``` +→ category_CategoryService_CreateCategory({"name":"Work","user_id":"user1"}) +← {"record":{"id":"f633...","name":"Work"},"success":true} +→ task_TaskService_CreateTask({"title":"Finish report","category_id":"f633..."}) + +Created Work category and added 'Finish report' task to it. +``` + +No service-to-service calls. No distributed transactions. No saga patterns. The agent is the orchestrator — it reads the result of one call and uses it as input to the next. Each service stays simple, stateless, and independently deployable. + +This is the answer to the oldest problem in microservices: how do services coordinate? They don't. An intelligent agent does it for them. + +## Iteration Without Destruction + +Run the same command again — services whose proto hasn't changed are skipped entirely. Edit a handler by hand, and re-running preserves your changes. The system tracks SHA-256 hashes of generated files and only regenerates what's needed. + +``` +micro new --prompt "add a tag-service for labeling todos" + ✓ category-service (unchanged) + ✓ task-service (unchanged) + ✓ tag-service ← new +``` + +The LLM sees existing service protos and extends the system rather than redesigning from scratch. + +## What This Isn't + +This isn't a no-code platform. The generated code is standard Go — you own it, edit it, deploy it however you want. There's no vendor lock-in, no runtime dependency on an AI provider, no magic abstraction layer. + +It's also not a replacement for thinking about your architecture. The LLM designs a starting point. You refine it. The services are real Go code with real interfaces — the same code you'd write by hand, generated in 30 seconds instead of 3 hours. + +## What This Changes + +The barrier to running microservices has always been boilerplate. Proto definitions, handler scaffolding, service registration, build systems, proto compilation — it takes hours before you can test a single endpoint. + +With `micro run --prompt`, you go from idea to running services in under a minute. The AI handles the ceremony. You handle the domain logic. + +``` +micro run --prompt "an order system for dropshipping" +micro chat --provider anthropic +> Place an order for 5 units of SKU-123 shipping to London +``` + +That's a running order system with real validation, inventory checks, and shipping calculations. Not a prototype. Not a demo. A service you can deploy, iterate on, and scale. + +## Try It + +```bash +go install go-micro.dev/v5/cmd/micro@latest + +# Generate and run +micro run --prompt "a task management system" --provider anthropic + +# Talk to your services +micro chat --provider anthropic +``` + +The future of microservices isn't less services. It's making them so easy to create and orchestrate that the architecture fades into the background. The services become tools. The agent becomes the interface. The developer focuses on what matters: the domain. + +--- + +*Go Micro is open source. Star us on [GitHub](https://github.com/micro/go-micro), join the [Discord](https://discord.gg/go-micro), or read the [docs](https://go-micro.dev/docs).* From a43740729c7ab6dcf6da63add87efbf89a782a8e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 18:58:56 +0000 Subject: [PATCH 15/19] fix: timeouts, max_tokens, TTY detection, smaller services - Add 60s timeout on design, 90s on handler generation, 60s on compile-fix LLM calls so hung providers don't block forever - Bump Anthropic max_tokens from 4096 to 8192 to reduce truncation - Add TTY detection: spinner prints static message in non-TTY (CI/pipes) instead of ANSI escape codes - Tighten prompts: max 200 lines per handler, 2-4 services, 5-8 fields, explicit "services don't call each other" rule https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- ai/anthropic/anthropic.go | 4 ++-- cmd/micro/cli/generate/generate.go | 37 ++++++++++++++++++++++++------ 2 files changed, 32 insertions(+), 9 deletions(-) diff --git a/ai/anthropic/anthropic.go b/ai/anthropic/anthropic.go index 5f2b5d0896..5c8787b017 100644 --- a/ai/anthropic/anthropic.go +++ b/ai/anthropic/anthropic.go @@ -77,7 +77,7 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen // Build initial request apiReq := map[string]any{ "model": p.opts.Model, - "max_tokens": 4096, + "max_tokens": 8192, "system": req.SystemPrompt, "messages": []map[string]any{ {"role": "user", "content": req.Prompt}, @@ -127,7 +127,7 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen followUpReq := map[string]any{ "model": p.opts.Model, - "max_tokens": 4096, + "max_tokens": 8192, "system": req.SystemPrompt, "messages": messages, } diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index 01129c9565..e47b1dd7c7 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -49,12 +49,14 @@ Return ONLY valid JSON: Rules: - Service names are lowercase, hyphenated - Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List -- Add custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory, CalculateShipping) +- Add 1-3 custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory) - Field types: string, int64, bool, float64 - Every service needs id (string), created (int64), updated (int64) fields - Endpoint names are PascalCase - Examples should be realistic JSON -- 2-5 services max, focused on the domain` +- 2-4 services max, focused on the domain +- Keep services small and focused — one concern per service, max 5-8 fields +- Services don't call each other; an AI agent orchestrates across them` const designPromptWithExisting = `You are a Go microservices architect using the go-micro framework. The user has an EXISTING system with services already running. They want to extend or modify it. @@ -105,6 +107,7 @@ The handler must: 7. Every exported method must have a doc comment explaining what it does 8. Every method must have an @example tag with realistic JSON input 9. Handle edge cases, validation, and return meaningful errors +10. Keep the file under 200 lines — be concise, no boilerplate The struct name is %s. The constructor is func New() *%s. @@ -162,7 +165,9 @@ func Design(ctx context.Context, provider, apiKey, model, baseDir, prompt string } sp := startSpinner("designing services...") - resp, err := m.Generate(ctx, &ai.Request{ + designCtx, designCancel := context.WithTimeout(ctx, 60*time.Second) + defer designCancel() + resp, err := m.Generate(designCtx, &ai.Request{ Prompt: userPrompt, SystemPrompt: sysPrompt, }) @@ -349,7 +354,9 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe svc.Name, titleName, titleName, proto, strings.Join(epDescs, "\n")) sp := startSpinner(fmt.Sprintf("writing %s handler...", svc.Name)) - resp, err := m.Generate(ctx, &ai.Request{ + genCtx, genCancel := context.WithTimeout(ctx, 90*time.Second) + defer genCancel() + resp, err := m.Generate(genCtx, &ai.Request{ Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic.", svc.Name), SystemPrompt: prompt, }) @@ -368,8 +375,10 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe if isTruncated(code) { fmt.Printf(" \033[33m→\033[0m response truncated, retrying...\n") sp = startSpinner(fmt.Sprintf("rewriting %s handler...", svc.Name)) - resp, err = m.Generate(ctx, &ai.Request{ - Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic. Keep it concise — no more than 300 lines.", svc.Name), + retryCtx, retryCancel := context.WithTimeout(ctx, 90*time.Second) + defer retryCancel() + resp, err = m.Generate(retryCtx, &ai.Request{ + Prompt: fmt.Sprintf("Generate the handler for the %s service with real business logic. Keep it concise — no more than 200 lines.", svc.Name), SystemPrompt: prompt, }) sp.Stop() @@ -409,11 +418,13 @@ func compileFix(ctx context.Context, m ai.Model, dir, name string, maxAttempts i currentCode := readFile(handlerPath) sp := startSpinner(fmt.Sprintf("fixing compile errors (attempt %d/%d)...", attempt+1, maxAttempts)) - resp, fixErr := m.Generate(ctx, &ai.Request{ + fixCtx, fixCancel := context.WithTimeout(ctx, 60*time.Second) + resp, fixErr := m.Generate(fixCtx, &ai.Request{ Prompt: fmt.Sprintf("This Go code has compile errors. Fix ALL of them and return the COMPLETE corrected file.\n\nErrors:\n%s\n\nCode:\n%s", string(out), currentCode), SystemPrompt: "You are a Go expert. Return ONLY the corrected Go code. No markdown, no explanation. Start with 'package handler'.", }) + fixCancel() sp.Stop() if fixErr != nil { return fmt.Errorf("fix attempt failed: %w", fixErr) @@ -665,8 +676,20 @@ type spinner struct { done sync.WaitGroup } +func isTTY() bool { + fi, err := os.Stdout.Stat() + if err != nil { + return false + } + return fi.Mode()&os.ModeCharDevice != 0 +} + func startSpinner(msg string) *spinner { s := &spinner{msg: msg, stop: make(chan struct{})} + if !isTTY() { + fmt.Printf(" %s\n", msg) + return s + } s.done.Add(1) go func() { defer s.done.Done() From d6576e53e8d28d6df9b729273c43a4731cca76c8 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 19:01:11 +0000 Subject: [PATCH 16/19] feat: chat suggests creating services when capabilities are missing Update system prompt with the list of available services. When the user asks for something no existing service can handle, the agent explains what's available and suggests the exact micro new --prompt command to create the missing service. This is the natural evolution path: start with a few services, talk to them via chat, and when the domain grows, the agent tells you what to add. Each service stays small and focused. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/chat/chat.go | 32 +++++++++++++++++++++++++------- 1 file changed, 25 insertions(+), 7 deletions(-) diff --git a/cmd/micro/chat/chat.go b/cmd/micro/chat/chat.go index 954a9185a3..5931708e2f 100644 --- a/cmd/micro/chat/chat.go +++ b/cmd/micro/chat/chat.go @@ -30,9 +30,13 @@ import ( _ "go-micro.dev/v5/ai/together" ) -const systemPrompt = "You are an agent that helps users interact with microservices. " + - "Use the available tools to fulfill user requests. " + - "When you call a tool, explain what you are doing." +const systemPromptTmpl = `You are an agent that orchestrates microservices. Use the available tools to fulfill user requests. When you call a tool, explain what you are doing. + +Available services: %s + +If a user asks for something that no existing service can handle, tell them which service they need and suggest the command to create it. For example: "You don't have a shipping service yet. Run: micro new --prompt 'add a shipping service' to create one." + +Do NOT make up capabilities. Only use the tools that are available.` func init() { cmd.Register(&cli.Command{ @@ -119,8 +123,22 @@ func run(c *cli.Context) error { hist := ai.NewHistory(50) + // Build service list for system prompt + serviceNames := make(map[string]bool) + for _, t := range discovered { + parts := strings.SplitN(t.OriginalName, ".", 2) + if len(parts) == 2 { + serviceNames[parts[0]] = true + } + } + var svcList []string + for name := range serviceNames { + svcList = append(svcList, name) + } + sysPrompt := fmt.Sprintf(systemPromptTmpl, strings.Join(svcList, ", ")) + if singlePrompt != "" { - return ask(c.Context, m, hist, discovered, singlePrompt) + return ask(c.Context, m, hist, discovered, sysPrompt, singlePrompt) } // Startup banner @@ -162,19 +180,19 @@ func run(c *cli.Context) error { fmt.Println() continue } - if err := ask(c.Context, m, hist, discovered, line); err != nil { + if err := ask(c.Context, m, hist, discovered, sysPrompt, line); err != nil { fmt.Printf("\033[31merror:\033[0m %v\n", err) } fmt.Println() } } -func ask(ctx context.Context, m ai.Model, hist *ai.History, toolList []ai.Tool, prompt string) error { +func ask(ctx context.Context, m ai.Model, hist *ai.History, toolList []ai.Tool, sysPrompt, prompt string) error { hist.Add("user", prompt) resp, err := m.Generate(ctx, &ai.Request{ Prompt: prompt, - SystemPrompt: systemPrompt, + SystemPrompt: sysPrompt, Tools: toolList, Messages: hist.Messages(), }) From ddeab719d6834f49cc7dbc51ddbd339e9cbf92d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 19:16:05 +0000 Subject: [PATCH 17/19] feat: chat generates and starts services inline, drop -service suffix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Chat agent now has a micro_generate_service tool. When the user asks for a capability that doesn't exist, the agent generates the service, compiles it, starts it as a background process, waits for registration, re-discovers tools, and uses the new endpoints immediately — all within the conversation. Service naming: design prompt now instructs LLM to return names without '-service' suffix (e.g. 'task' not 'task-service'). buildMain keeps TrimSuffix as safety net for backward compatibility. Spawned processes are cleaned up when chat exits. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- cmd/micro/chat/chat.go | 253 +++++++++++++++++------- cmd/micro/cli/generate/generate.go | 4 +- cmd/micro/cli/generate/generate_test.go | 19 +- 3 files changed, 206 insertions(+), 70 deletions(-) diff --git a/cmd/micro/chat/chat.go b/cmd/micro/chat/chat.go index 5931708e2f..3fa1b2aa47 100644 --- a/cmd/micro/chat/chat.go +++ b/cmd/micro/chat/chat.go @@ -12,15 +12,18 @@ import ( "encoding/json" "fmt" "os" + "os/exec" + "path/filepath" "strings" + "time" "github.com/urfave/cli/v2" "go-micro.dev/v5/ai" - "go-micro.dev/v5/client" + clt "go-micro.dev/v5/client" "go-micro.dev/v5/cmd" + "go-micro.dev/v5/cmd/micro/cli/generate" "go-micro.dev/v5/registry" - // Side-effect imports register the AI providers. _ "go-micro.dev/v5/ai/anthropic" _ "go-micro.dev/v5/ai/atlascloud" _ "go-micro.dev/v5/ai/gemini" @@ -34,9 +37,21 @@ const systemPromptTmpl = `You are an agent that orchestrates microservices. Use Available services: %s -If a user asks for something that no existing service can handle, tell them which service they need and suggest the command to create it. For example: "You don't have a shipping service yet. Run: micro new --prompt 'add a shipping service' to create one." +If a user asks for something that no existing service can handle, use the micro_generate_service tool to create it. Pass a short description of what the service should do. After it's created, the new service's endpoints will be available as tools and you can use them immediately. -Do NOT make up capabilities. Only use the tools that are available.` +Do NOT make up capabilities. Only use the tools that are available. If generation fails, tell the user.` + +var generateTool = ai.Tool{ + Name: "micro_generate_service", + OriginalName: "micro.generate_service", + Description: "Generate a new microservice from a description. Use when the user needs a capability that no existing service provides. The service will be created, compiled, and started automatically.", + Properties: map[string]any{ + "description": map[string]any{ + "type": "string", + "description": "What the service should do, e.g. 'a shipping service that tracks parcels and calculates rates'", + }, + }, +} func init() { cmd.Register(&cli.Command{ @@ -49,24 +64,12 @@ tool, and lets you ask natural-language questions like "list all users" or "create an order for product 42". The model decides which tool to call and issues RPCs to the right service. +If you ask for something no existing service handles, the agent will generate +a new service automatically and start using it. + Examples: - # Chat with Anthropic Claude (uses ANTHROPIC_API_KEY) ANTHROPIC_API_KEY=sk-ant-... micro chat --provider anthropic - - # Use a single prompt and exit - micro chat --provider openai --prompt "list all users" - - # Use a custom provider via base URL (auto-detected) - micro chat --api_key $KEY --base_url https://api.groq.com/openai - -Environment variables: - MICRO_AI_PROVIDER Provider name (anthropic, openai, gemini, groq, ...) - MICRO_AI_API_KEY API key for the provider - MICRO_AI_MODEL Model name override - MICRO_AI_BASE_URL Base URL override - ANTHROPIC_API_KEY Fallback API key for the anthropic provider - OPENAI_API_KEY Fallback API key for the openai provider - GEMINI_API_KEY Fallback API key for the gemini provider`, + micro chat --provider openai --prompt "list all users"`, Flags: []cli.Flag{ &cli.StringFlag{Name: "provider", Usage: "AI provider (anthropic, openai, gemini, groq, mistral, together, atlascloud)", EnvVars: []string{"MICRO_AI_PROVIDER"}}, &cli.StringFlag{Name: "api_key", Usage: "API key for the provider", EnvVars: []string{"MICRO_AI_API_KEY"}}, @@ -78,10 +81,123 @@ Environment variables: }) } +type session struct { + provider string + apiKey string + model ai.Model + tools *ai.Tools + reg registry.Registry + hist *ai.History + toolList []ai.Tool + sysPrompt string + procs []*exec.Cmd +} + +func (s *session) refreshTools() { + discovered, err := s.tools.Discover() + if err != nil { + return + } + s.toolList = append(discovered, generateTool) + + serviceNames := make(map[string]bool) + for _, t := range discovered { + parts := strings.SplitN(t.OriginalName, ".", 2) + if len(parts) == 2 { + serviceNames[parts[0]] = true + } + } + var svcList []string + for name := range serviceNames { + svcList = append(svcList, name) + } + if len(svcList) == 0 { + s.sysPrompt = fmt.Sprintf(systemPromptTmpl, "(none yet)") + } else { + s.sysPrompt = fmt.Sprintf(systemPromptTmpl, strings.Join(svcList, ", ")) + } +} + +func (s *session) handleGenerate(input map[string]any) (any, string) { + desc, _ := input["description"].(string) + if desc == "" { + return map[string]string{"error": "description is required"}, `{"error":"description is required"}` + } + + fmt.Printf("\n \033[36m⚡\033[0m generating service: %s\n", desc) + + design, err := generate.Design(context.Background(), s.provider, s.apiKey, "", ".", desc) + if err != nil { + msg := fmt.Sprintf(`{"error":"design failed: %s"}`, err) + return map[string]string{"error": err.Error()}, msg + } + + if err := generate.Generate(context.Background(), ".", design, s.provider, s.apiKey, ""); err != nil { + msg := fmt.Sprintf(`{"error":"generate failed: %s"}`, err) + return map[string]string{"error": err.Error()}, msg + } + + // Find which services are new (not already in registry) + existing := make(map[string]bool) + if svcs, err := s.reg.ListServices(); err == nil { + for _, svc := range svcs { + existing[svc.Name] = true + } + } + + var created []string + for _, svc := range design.Services { + name := strings.TrimSuffix(svc.Name, "-service") + if existing[name] { + continue + } + created = append(created, svc.Name) + + // Build and start the new service + svcDir, _ := filepath.Abs(svc.Name) + fmt.Printf(" \033[36m⚡\033[0m starting %s...\n", svc.Name) + + buildCmd := exec.Command("go", "build", "-o", svc.Name, ".") + buildCmd.Dir = svcDir + if out, err := buildCmd.CombinedOutput(); err != nil { + fmt.Printf(" \033[33m⚠\033[0m build failed: %s\n", string(out)) + continue + } + + runCmd := exec.Command(filepath.Join(svcDir, svc.Name)) + runCmd.Dir = svcDir + if err := runCmd.Start(); err != nil { + fmt.Printf(" \033[33m⚠\033[0m start failed: %v\n", err) + continue + } + s.procs = append(s.procs, runCmd) + } + + if len(created) == 0 { + result := map[string]any{"message": "No new services needed — all already exist."} + b, _ := json.Marshal(result) + return result, string(b) + } + + // Wait for services to register + fmt.Printf(" \033[36m⚡\033[0m waiting for services to register...\n") + time.Sleep(5 * time.Second) + + s.refreshTools() + fmt.Printf(" \033[32m✓\033[0m %d tools available\n\n", len(s.toolList)-1) + + result := map[string]any{ + "created": created, + "message": fmt.Sprintf("Created and started: %s. Their endpoints are now available as tools.", strings.Join(created, ", ")), + } + b, _ := json.Marshal(result) + return result, string(b) +} + func run(c *cli.Context) error { provider := c.String("provider") apiKey := c.String("api_key") - model := c.String("model") + modelName := c.String("model") baseURL := c.String("base_url") singlePrompt := c.String("prompt") @@ -95,64 +211,62 @@ func run(c *cli.Context) error { return fmt.Errorf("no API key configured; set --api_key or %s", envVarForProvider(provider)) } - // Discover tools and wire up a handler that routes to the right RPC. reg := registry.DefaultRegistry - cli := client.DefaultClient + cl := clt.DefaultClient - tools := ai.NewTools(reg, ai.ToolClient(cli)) - discovered, err := tools.Discover() - if err != nil { - return fmt.Errorf("discover tools: %w", err) + tools := ai.NewTools(reg, ai.ToolClient(cl)) + + s := &session{ + provider: provider, + apiKey: apiKey, + tools: tools, + reg: reg, + hist: ai.NewHistory(50), + } + s.refreshTools() + + // Wrap the tool handler to intercept generate calls + baseHandler := tools.Handler() + wrappedHandler := func(name string, input map[string]any) (any, string) { + if name == "micro_generate_service" { + return s.handleGenerate(input) + } + return baseHandler(name, input) } opts := []ai.Option{ ai.WithAPIKey(apiKey), - ai.WithTools(tools), + ai.WithToolHandler(wrappedHandler), } - if model != "" { - opts = append(opts, ai.WithModel(model)) + if modelName != "" { + opts = append(opts, ai.WithModel(modelName)) } if baseURL != "" { opts = append(opts, ai.WithBaseURL(baseURL)) } - m := ai.New(provider, opts...) - if m == nil { + s.model = ai.New(provider, opts...) + if s.model == nil { return fmt.Errorf("unknown provider: %s", provider) } - hist := ai.NewHistory(50) - - // Build service list for system prompt - serviceNames := make(map[string]bool) - for _, t := range discovered { - parts := strings.SplitN(t.OriginalName, ".", 2) - if len(parts) == 2 { - serviceNames[parts[0]] = true - } - } - var svcList []string - for name := range serviceNames { - svcList = append(svcList, name) - } - sysPrompt := fmt.Sprintf(systemPromptTmpl, strings.Join(svcList, ", ")) + defer s.cleanup() if singlePrompt != "" { - return ask(c.Context, m, hist, discovered, sysPrompt, singlePrompt) + return s.ask(c.Context, singlePrompt) } - // Startup banner fmt.Println() fmt.Println(" \033[1mmicro chat\033[0m") fmt.Println() fmt.Printf(" Provider \033[36m%s\033[0m\n", provider) - fmt.Printf(" Model \033[36m%s\033[0m\n", m.Options().Model) + fmt.Printf(" Model \033[36m%s\033[0m\n", s.model.Options().Model) fmt.Println() fmt.Println(" Tools:") - for _, t := range discovered { + for _, t := range s.toolList { fmt.Printf(" \033[32m●\033[0m %s\n", t.OriginalName) } - if len(discovered) == 0 { + if len(s.toolList) == 0 { fmt.Println(" \033[33m(no services found)\033[0m") } fmt.Println() @@ -175,42 +289,45 @@ func run(c *cli.Context) error { return nil } if line == "reset" { - hist.Reset() + s.hist.Reset() fmt.Println("\033[2m(history cleared)\033[0m") fmt.Println() continue } - if err := ask(c.Context, m, hist, discovered, sysPrompt, line); err != nil { + if err := s.ask(c.Context, line); err != nil { fmt.Printf("\033[31merror:\033[0m %v\n", err) } fmt.Println() } } -func ask(ctx context.Context, m ai.Model, hist *ai.History, toolList []ai.Tool, sysPrompt, prompt string) error { - hist.Add("user", prompt) +func (s *session) ask(ctx context.Context, prompt string) error { + s.hist.Add("user", prompt) - resp, err := m.Generate(ctx, &ai.Request{ + resp, err := s.model.Generate(ctx, &ai.Request{ Prompt: prompt, - SystemPrompt: sysPrompt, - Tools: toolList, - Messages: hist.Messages(), + SystemPrompt: s.sysPrompt, + Tools: s.toolList, + Messages: s.hist.Messages(), }) if err != nil { return err } if resp.Reply != "" { - hist.Add("assistant", resp.Reply) + s.hist.Add("assistant", resp.Reply) } if resp.Answer != "" { - hist.Add("assistant", resp.Answer) + s.hist.Add("assistant", resp.Answer) } if resp.Reply != "" { fmt.Println(resp.Reply) } for _, tc := range resp.ToolCalls { + if tc.Name == "micro_generate_service" { + continue // output handled by handleGenerate + } args, _ := json.Marshal(tc.Input) fmt.Printf(" \033[33m→\033[0m \033[2m%s\033[0m(%s)\n", tc.Name, args) if tc.Result != "" { @@ -227,10 +344,14 @@ func ask(ctx context.Context, m ai.Model, hist *ai.History, toolList []ai.Tool, return nil } -// fallbackAPIKey returns the provider-specific environment variable when -// neither --api_key nor MICRO_AI_API_KEY is set. This lets users keep -// existing ANTHROPIC_API_KEY / OPENAI_API_KEY / GEMINI_API_KEY vars -// without re-exporting them. +func (s *session) cleanup() { + for _, p := range s.procs { + if p.Process != nil { + p.Process.Kill() + } + } +} + func fallbackAPIKey(provider string) string { if v := os.Getenv(envVarForProvider(provider)); v != "" { return v diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index e47b1dd7c7..d8e68587dc 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -47,7 +47,7 @@ Return ONLY valid JSON: } Rules: -- Service names are lowercase, hyphenated +- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service") - Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List - Add 1-3 custom endpoints for real business logic (e.g. PlaceOrder, CheckInventory) - Field types: string, int64, bool, float64 @@ -85,7 +85,7 @@ Return ONLY valid JSON: } Rules: -- Service names are lowercase, hyphenated +- Service names are lowercase, hyphenated, WITHOUT a "-service" suffix (e.g. "task" not "task-service", "shipping" not "shipping-service") - Each service MUST have CRUD endpoints: Create, Read, Update, Delete, List - Add custom endpoints for real business logic - Field types: string, int64, bool, float64 diff --git a/cmd/micro/cli/generate/generate_test.go b/cmd/micro/cli/generate/generate_test.go index 86af3f25c6..7a1251b27b 100644 --- a/cmd/micro/cli/generate/generate_test.go +++ b/cmd/micro/cli/generate/generate_test.go @@ -178,9 +178,24 @@ func TestBuildProto(t *testing.T) { } func TestBuildMain(t *testing.T) { - main := buildMain("order-service", "OrderService") - + // New naming: no -service suffix + main := buildMain("order", "Order") checks := []string{ + `"order/handler"`, + `pb "order/proto"`, + `micro.New("order"`, + `pb.RegisterOrderHandler`, + `handler.New()`, + } + for _, c := range checks { + if !strings.Contains(main, c) { + t.Errorf("buildMain(order) missing %q", c) + } + } + + // Legacy naming: -service suffix stripped + main = buildMain("order-service", "OrderService") + checks = []string{ `"order-service/handler"`, `pb "order-service/proto"`, `micro.New("order"`, From c5f19966acd7be651e10e9fb41313881fa8a7777 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 19:18:24 +0000 Subject: [PATCH 18/19] docs: rewrite blog post 13 with inline service generation Updated to reflect the full UX: services generate and start within the chat conversation. Added the shipping example showing the agent creating a service mid-conversation. Removed -service suffix from all examples. Tightened the narrative around agent-as-orchestrator. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- internal/website/blog/13.md | 112 +++++++++++++++++++++++------------- 1 file changed, 73 insertions(+), 39 deletions(-) diff --git a/internal/website/blog/13.md b/internal/website/blog/13.md index 348d659538..0b36c1371f 100644 --- a/internal/website/blog/13.md +++ b/internal/website/blog/13.md @@ -2,45 +2,45 @@ layout: blog title: "From Prompt to Production: AI-Generated Microservices That Actually Run" permalink: /blog/13 -description: "micro run --prompt generates real services with business logic, compiles them, starts them, and lets you talk to them. Here's how it works and why it matters." +description: "micro run --prompt generates real services with business logic, compiles them, starts them, and lets you talk to them. When you need more, the agent builds new services mid-conversation." --- # From Prompt to Production: AI-Generated Microservices That Actually Run *June 3, 2026 • By the Go Micro Team* -Every code generator promises the same thing: describe what you want, get code out. The reality is always a skeleton — empty handlers, TODO comments, and hours of work before anything actually runs. - -We built something different. One command generates real microservices with business logic, compiles them, starts them, and lets you talk to them through an AI agent. No intermediate steps. +Every code generator stops at the same point: here's some files, good luck. You get a skeleton, wire it up yourself, hope it compiles. We wanted something different. ``` -micro run --prompt "a todo list with tasks and categories" +micro run --prompt "a task management system" ``` -That's it. Two services start, register, and respond to requests. An AI agent can create tasks, list categories, and orchestrate across services immediately. +That's not scaffolding. Two services start, register, and respond to requests. An AI agent can create tasks, organize categories, and orchestrate across services immediately. And when you need a capability that doesn't exist — shipping, payments, notifications — the agent builds it mid-conversation. -## What Actually Happens +## The Full Loop -The flow has four stages, each solving a problem that previous generators ignore. +**1. Describe** — tell it what you need in plain English. -**1. Design** — an LLM architects the system from your description. It returns a structured JSON spec with service names, fields, endpoints, and example inputs. You see the design and confirm before anything is generated. +``` +micro run --prompt "a todo list with tasks and categories" +``` + +**2. Review** — the LLM designs the architecture. You see every service, field, and endpoint before a line of code is written. ``` Services: ● category — Manages task categories - CreateCategory, ReadCategory, ListCategories, ... + Create, Read, Update, Delete, List ● task — Core task management - CreateTask, CompleteTask, GetOverdueTasks, ... + Create, Read, Update, Delete, List, CompleteTask, GetOverdue Generate? [Y/n] ``` -**2. Generate** — for each service, proto files are written deterministically from the spec. Then the LLM writes the handler with real business logic: input validation, edge cases, thread-safe in-memory storage, doc comments, and `@example` tags that become MCP tool descriptions. - -Here's what the generated `CompleteTask` handler looks like — not a TODO, not a stub: +**3. Generate** — proto files are written deterministically from the spec. Then the LLM writes each handler with real business logic. Not stubs. Not TODOs. Actual validation, edge cases, thread-safe storage: ```go -func (s *TaskService) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, +func (s *Task) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, rsp *pb.CompleteTaskResponse) error { if req.Id == "" { return errors.New("id is required") @@ -60,62 +60,99 @@ func (s *TaskService) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequ task.Updated = time.Now().Unix() rsp.Record = task rsp.Success = true - rsp.Message = "task marked as completed" return nil } ``` -**3. Compile-fix** — every generated handler is compiled immediately. If it fails, the errors are fed back to the LLM for correction, up to three iterations. If the response was truncated (unbalanced braces), it retries with a conciseness hint. Most services compile on the first attempt. +Every handler compiles. If it doesn't, the errors are fed back to the LLM for correction. Most services compile on the first attempt. -**4. Run** — `micro run` builds each service, starts them as separate processes, registers them via mDNS, and launches an HTTP gateway. Every endpoint is immediately accessible via REST, gRPC, or MCP. +**4. Run** — services start, register via mDNS, and an HTTP gateway comes up. Every endpoint is accessible via REST, gRPC, or MCP. -## The Agent Layer +``` +Micro + + Dashboard http://localhost:8080 + API http://localhost:8080/api/{service}/{method} + + Services: + ● category + ● task -The real insight isn't code generation. It's what happens after the services start. + micro chat --provider anthropic # talk to your services +``` + +## Talk To Your Services ``` micro chat --provider anthropic > Create a Work category, then add a task called 'Finish report' to it ``` -The agent discovers both services from the registry, sees every endpoint as a tool, and orchestrates: +The agent discovers services from the registry, sees every endpoint as a tool, and orchestrates: ``` -→ category_CategoryService_CreateCategory({"name":"Work","user_id":"user1"}) +→ category_Category_Create({"name":"Work","user_id":"user1"}) ← {"record":{"id":"f633...","name":"Work"},"success":true} -→ task_TaskService_CreateTask({"title":"Finish report","category_id":"f633..."}) +→ task_Task_Create({"title":"Finish report","category_id":"f633..."}) +← {"record":{"id":"a1b2...","title":"Finish report","status":"pending"}} Created Work category and added 'Finish report' task to it. ``` -No service-to-service calls. No distributed transactions. No saga patterns. The agent is the orchestrator — it reads the result of one call and uses it as input to the next. Each service stays simple, stateless, and independently deployable. +No service-to-service calls. No distributed transactions. No saga patterns. The agent reads the result of one call and uses it as input to the next. Each service stays simple and independent. This is the answer to the oldest problem in microservices: how do services coordinate? They don't. An intelligent agent does it for them. +## Growing the System + +Here's where it gets interesting. You're chatting, the domain grows, and you need something that doesn't exist: + +``` +> I need to track shipping for my orders. Create a shipment for order 123 to London. + + ⚡ generating service: a shipping service... + ✓ task (unchanged) + ✓ category (unchanged) + ✓ shipping + ⚡ starting shipping... + ✓ 13 tools available + + → shipping_Shipping_Create({"order_id":"123","destination":"London"}) + ← {"record":{"id":"xyz...","status":"pending"}} + + Created shipment for order 123 going to London. +``` + +The agent recognised that no shipping service exists, generated one, compiled it, started it, discovered its endpoints, and used them — all within the conversation. You didn't leave the chat. You didn't run a separate command. The system grew because you needed it to. + +Each service stays small and focused. When you need more, you add more services. The agent orchestrates across whatever exists. + ## Iteration Without Destruction -Run the same command again — services whose proto hasn't changed are skipped entirely. Edit a handler by hand, and re-running preserves your changes. The system tracks SHA-256 hashes of generated files and only regenerates what's needed. +Run the same prompt again — services whose proto hasn't changed are skipped entirely: ``` -micro new --prompt "add a tag-service for labeling todos" - ✓ category-service (unchanged) - ✓ task-service (unchanged) - ✓ tag-service ← new + ✓ category (unchanged) + ✓ task (unchanged) ``` +Edit a handler by hand, and re-running preserves your changes. The system tracks SHA-256 hashes of generated files and only regenerates what's actually different. + The LLM sees existing service protos and extends the system rather than redesigning from scratch. ## What This Isn't -This isn't a no-code platform. The generated code is standard Go — you own it, edit it, deploy it however you want. There's no vendor lock-in, no runtime dependency on an AI provider, no magic abstraction layer. +This isn't a no-code platform. The generated code is standard Go — you own it, edit it, version it, deploy it however you want. There's no vendor lock-in, no runtime dependency on an AI provider, no magic abstraction. + +The services are real Go code with real interfaces. The same code you'd write by hand, generated in 30 seconds instead of 3 hours. -It's also not a replacement for thinking about your architecture. The LLM designs a starting point. You refine it. The services are real Go code with real interfaces — the same code you'd write by hand, generated in 30 seconds instead of 3 hours. +## Why This Matters -## What This Changes +The barrier to microservices has always been ceremony. Proto definitions, handler scaffolding, service registration, build systems — hours of work before you can test a single endpoint. -The barrier to running microservices has always been boilerplate. Proto definitions, handler scaffolding, service registration, build systems, proto compilation — it takes hours before you can test a single endpoint. +The deeper problem was coordination. Services need to talk to each other, and every pattern for that (sagas, choreography, service mesh) adds complexity. Teams spend more time on infrastructure than on business logic. -With `micro run --prompt`, you go from idea to running services in under a minute. The AI handles the ceremony. You handle the domain logic. +`micro run --prompt` solves both. The AI handles the ceremony. The agent handles the coordination. You handle the domain. ``` micro run --prompt "an order system for dropshipping" @@ -123,21 +160,18 @@ micro chat --provider anthropic > Place an order for 5 units of SKU-123 shipping to London ``` -That's a running order system with real validation, inventory checks, and shipping calculations. Not a prototype. Not a demo. A service you can deploy, iterate on, and scale. +That's a running system. Not a prototype. Not a demo. Services you can deploy, iterate on, and scale. ## Try It ```bash go install go-micro.dev/v5/cmd/micro@latest -# Generate and run micro run --prompt "a task management system" --provider anthropic - -# Talk to your services micro chat --provider anthropic ``` -The future of microservices isn't less services. It's making them so easy to create and orchestrate that the architecture fades into the background. The services become tools. The agent becomes the interface. The developer focuses on what matters: the domain. +The future of microservices isn't fewer services. It's making them so easy to create and compose that the architecture disappears. Services become tools. The agent becomes the interface. You focus on what matters: the domain. --- From a141bdd084b18916da2fe7415b510df5fc254a17 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 3 Jun 2026 19:25:11 +0000 Subject: [PATCH 19/19] feat: persistent storage, README quickstart, auto-detect new services MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Storage: generated handlers now use go-micro's store package instead of in-memory maps. Data persists across restarts. The handler prompt includes store API examples so the LLM generates correct store usage. README: added "Generate From a Prompt" section with micro run --prompt and micro chat examples, linking to blog post 13. Watcher: micro run now scans for new service directories every 5s. When micro chat generates a service, micro run detects the new directory, builds it, starts it, and adds it to the watcher — fully automatic. Added AddDir/Dirs methods to the watcher. Blog: updated post 13 with persistent storage example and watcher note. https://claude.ai/code/session_01QTp4SshuVmLAvvEGJe4TJd --- README.md | 18 ++++++++- cmd/micro/cli/generate/generate.go | 32 ++++++++++++++-- cmd/micro/run/run.go | 59 ++++++++++++++++++++++++++++++ cmd/micro/run/watcher/watcher.go | 19 ++++++++++ internal/website/blog/13.md | 18 +++++---- 5 files changed, 134 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 5389fd5614..bfbad7285e 100644 --- a/README.md +++ b/README.md @@ -314,13 +314,29 @@ go install go-micro.dev/v5/cmd/micro@v5.16.0 ```bash micro new helloworld # Create a new service -micro new contacts --template crud # Or use a template (crud, pubsub, api) cd helloworld micro run # Run with API gateway and hot reload ``` Then open http://localhost:8080 to see your service and call it from the browser. +### Generate From a Prompt + +Describe what you need in plain English. The AI designs services, writes handlers with real business logic, compiles them, and starts them: + +```bash +micro run --prompt "a task management system with categories" --provider anthropic +``` + +Then talk to your services through an agent: + +```bash +micro chat --provider anthropic +> Create a Work category, then add a task called 'Finish report' to it +``` + +The agent orchestrates across services automatically. When you need a capability that doesn't exist, the agent generates a new service mid-conversation. [Read more](https://go-micro.dev/blog/13). + ### Development Workflow | Stage | Command | Purpose | diff --git a/cmd/micro/cli/generate/generate.go b/cmd/micro/cli/generate/generate.go index d8e68587dc..d666622694 100644 --- a/cmd/micro/cli/generate/generate.go +++ b/cmd/micro/cli/generate/generate.go @@ -102,13 +102,39 @@ The handler must: 2. Import the proto package as: pb "%s/proto" 3. Import go-micro logger as: log "go-micro.dev/v5/logger" 4. Import "github.com/google/uuid" for ID generation -5. Use sync.RWMutex for thread-safe in-memory storage -6. Include REAL business logic — not just CRUD map operations +5. Use "go-micro.dev/v5/store" for persistent storage (NOT in-memory maps) +6. Include REAL business logic — not just CRUD store operations 7. Every exported method must have a doc comment explaining what it does 8. Every method must have an @example tag with realistic JSON input 9. Handle edge cases, validation, and return meaningful errors 10. Keep the file under 200 lines — be concise, no boilerplate +For storage, use the go-micro store package: + import "go-micro.dev/v5/store" + import "encoding/json" + + // In the struct: + store store.Store + + // In the constructor: + func New() *%s { return &%s{store: store.DefaultStore} } + + // Write a record: + data, _ := json.Marshal(record) + store.Write(&store.Record{Key: "prefix/" + id, Value: data}) + + // Read a record: + recs, err := store.Read("prefix/" + id) + json.Unmarshal(recs[0].Value, &record) + + // List keys: + keys, _ := store.List(store.ListPrefix("prefix/")) + + // Delete: + store.Delete("prefix/" + id) + +Do NOT use sync.Mutex or in-memory maps. Use store for all data. + The struct name is %s. The constructor is func New() *%s. @@ -351,7 +377,7 @@ func generateHandler(ctx context.Context, m ai.Model, dir string, svc ServiceSpe } prompt := fmt.Sprintf(handlerPrompt, - svc.Name, titleName, titleName, proto, strings.Join(epDescs, "\n")) + svc.Name, titleName, titleName, titleName, titleName, proto, strings.Join(epDescs, "\n")) sp := startSpinner(fmt.Sprintf("writing %s handler...", svc.Name)) genCtx, genCancel := context.WithTimeout(ctx, 90*time.Second) diff --git a/cmd/micro/run/run.go b/cmd/micro/run/run.go index 65eda17210..39bbffc856 100644 --- a/cmd/micro/run/run.go +++ b/cmd/micro/run/run.go @@ -393,6 +393,30 @@ func Run(c *cli.Context) error { } } }() + + // Scan for new services added by micro chat or micro new + go func() { + ticker := time.NewTicker(5 * time.Second) + defer ticker.Stop() + for { + select { + case <-sigCh: + return + case <-ticker.C: + newSvcs := discoverNewServices(absDir, servicesByDir, binDir, runDir, logsDir, envVars, len(services)) + for _, sp := range newSvcs { + services = append(services, sp) + servicesByDir[sp.dir] = sp + watch.AddDir(sp.dir) + if err := sp.start(logsDir); err != nil { + fmt.Fprintf(os.Stderr, "[%s] %v\n", sp.name, err) + continue + } + fmt.Printf("\n \033[32m●\033[0m %s \033[2m(new)\033[0m\n", sp.name) + } + } + } + }() } // Wait for signal @@ -433,6 +457,41 @@ func processRunning(pidStr string) bool { return proc.Signal(syscall.Signal(0)) == nil } +func discoverNewServices(baseDir string, known map[string]*serviceProcess, binDir, runDir, logsDir string, envVars []string, colorOffset int) []*serviceProcess { + var newSvcs []*serviceProcess + entries, err := os.ReadDir(baseDir) + if err != nil { + return nil + } + for _, e := range entries { + if !e.IsDir() { + continue + } + svcDir := filepath.Join(baseDir, e.Name()) + absSvcDir, _ := filepath.Abs(svcDir) + if _, exists := known[absSvcDir]; exists { + continue + } + mainFile := filepath.Join(svcDir, "main.go") + if _, err := os.Stat(mainFile); err != nil { + continue + } + name := e.Name() + hash := fmt.Sprintf("%x", md5.Sum([]byte(absSvcDir)))[:8] + sp := &serviceProcess{ + name: name, + dir: absSvcDir, + binPath: filepath.Join(binDir, name+"-"+hash), + pidFile: filepath.Join(runDir, name+"-"+hash+".pid"), + logFile: filepath.Join(logsDir, name+"-"+hash+".log"), + color: colorFor(colorOffset + len(newSvcs)), + env: envVars, + } + newSvcs = append(newSvcs, sp) + } + return newSvcs +} + func printBanner(services []*serviceProcess, gw *server.Gateway, watching bool, mcpAddr string) { fmt.Println() fmt.Println(" \033[1mMicro\033[0m") diff --git a/cmd/micro/run/watcher/watcher.go b/cmd/micro/run/watcher/watcher.go index 89d5bce59f..685a206db1 100644 --- a/cmd/micro/run/watcher/watcher.go +++ b/cmd/micro/run/watcher/watcher.go @@ -75,6 +75,25 @@ func (w *Watcher) Start() { go w.watch() } +// AddDir adds a new directory to watch +func (w *Watcher) AddDir(dir string) { + w.mu.Lock() + defer w.mu.Unlock() + for _, d := range w.dirs { + if d == dir { + return + } + } + w.dirs = append(w.dirs, dir) +} + +// Dirs returns the currently watched directories +func (w *Watcher) Dirs() []string { + w.mu.Lock() + defer w.mu.Unlock() + return append([]string{}, w.dirs...) +} + // Stop stops the watcher func (w *Watcher) Stop() { close(w.done) diff --git a/internal/website/blog/13.md b/internal/website/blog/13.md index 0b36c1371f..ee0692db95 100644 --- a/internal/website/blog/13.md +++ b/internal/website/blog/13.md @@ -37,7 +37,7 @@ Services: Generate? [Y/n] ``` -**3. Generate** — proto files are written deterministically from the spec. Then the LLM writes each handler with real business logic. Not stubs. Not TODOs. Actual validation, edge cases, thread-safe storage: +**3. Generate** — proto files are written deterministically from the spec. Then the LLM writes each handler with real business logic. Not stubs. Not TODOs. Actual validation, edge cases, and persistent storage via go-micro's built-in store: ```go func (s *Task) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, @@ -45,12 +45,12 @@ func (s *Task) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, if req.Id == "" { return errors.New("id is required") } - s.mu.Lock() - defer s.mu.Unlock() - task, exists := s.tasks[req.Id] - if !exists { + recs, err := s.store.Read("task/" + req.Id) + if err != nil || len(recs) == 0 { return errors.New("task not found") } + var task pb.TaskRecord + json.Unmarshal(recs[0].Value, &task) if task.Completed { rsp.Success = false rsp.Message = "task already completed" @@ -58,13 +58,15 @@ func (s *Task) CompleteTask(ctx context.Context, req *pb.CompleteTaskRequest, } task.Completed = true task.Updated = time.Now().Unix() - rsp.Record = task + data, _ := json.Marshal(&task) + s.store.Write(&store.Record{Key: "task/" + req.Id, Value: data}) + rsp.Record = &task rsp.Success = true return nil } ``` -Every handler compiles. If it doesn't, the errors are fed back to the LLM for correction. Most services compile on the first attempt. +Data survives restarts — generated services use go-micro's built-in store (file-backed by default, swappable to Postgres or NATS KV) instead of in-memory maps. Every handler compiles. If it doesn't, the errors are fed back to the LLM for correction. Most services compile on the first attempt. **4. Run** — services start, register via mDNS, and an HTTP gateway comes up. Every endpoint is accessible via REST, gRPC, or MCP. @@ -125,7 +127,7 @@ Here's where it gets interesting. You're chatting, the domain grows, and you nee The agent recognised that no shipping service exists, generated one, compiled it, started it, discovered its endpoints, and used them — all within the conversation. You didn't leave the chat. You didn't run a separate command. The system grew because you needed it to. -Each service stays small and focused. When you need more, you add more services. The agent orchestrates across whatever exists. +Each service stays small and focused. When you need more, you add more services. The agent orchestrates across whatever exists. If you're running `micro run`, it detects new service directories automatically and starts them — the loop is fully seamless. ## Iteration Without Destruction