// Package agent registers the 'micro agent' CLI commands. package agent import ( "context" "encoding/json" "fmt" "io" "os" "time" "github.com/urfave/cli/v2" goagent "go-micro.dev/v6/agent" "go-micro.dev/v6/cmd" aiflow "go-micro.dev/v6/flow" "go-micro.dev/v6/registry" "go-micro.dev/v6/store" ) const firstAgentQuickChecksHelp = `First-agent failure-mode quick checks Use this when scaffold -> run -> chat -> inspect stalls and you want the smallest provider-free recovery loop before reading the full docs. 1. Confirm prerequisites before starting the gateway: micro agent preflight 2. Start the project and keep it running in a separate terminal: micro run 3. Check the agent is registered and the chat gateway is reachable: micro agent doctor 4. If chat returns an answer or an error, inspect the latest run state: micro inspect agent micro runs 5. If provider chat is not configured yet, prove the no-secret path still works: micro agent demo go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1 go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1 Recovery docs: https://go-micro.dev/docs/guides/debugging-agents.html https://go-micro.dev/docs/guides/no-secret-first-agent.html` const noSecretDemoHelp = `No-secret first-agent demo Use this when you want the fastest provider-free agent success path before configuring API keys. It runs the maintained support/first-agent transcript with the deterministic mock model used by CI: go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1 What this proves: - service tools can be called by an agent - chat behavior is exercised without contacting a live provider - run history can be inspected after the prompt - the debug smoke seeds a stalled-first-agent recovery transcript After it passes: - Build your own service-backed agent: https://go-micro.dev/docs/guides/your-first-agent.html - Diagnose provider-backed chat: https://go-micro.dev/docs/guides/debugging-agents.html - Walk the full 0→hero lifecycle: https://go-micro.dev/docs/guides/zero-to-hero.html Use live-provider chat when you are ready for real model behavior: micro agent preflight # before micro run: prerequisites micro run micro chat micro agent doctor # after micro run: chat/gateway/inspect recovery micro inspect agent Debug transcript smoke: go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1` func init() { cmd.Register(&cli.Command{ Name: "runs", Usage: "Show recorded agent runs", ArgsUsage: "[agent] [run-id]", Flags: runFlags(), Action: func(c *cli.Context) error { name := c.Args().First() if name == "" { return fmt.Errorf("usage: micro runs [agent] [run-id]") } if runID := c.Args().Get(1); runID != "" { return printRunHistory(name, runID, c.Bool("json")) } return printRunIndex(name, runOptions(c), c.Bool("json")) }, }) cmd.Register(&cli.Command{ Name: "agent", Usage: "Manage AI agents (try: micro agent demo)", Subcommands: []*cli.Command{ { Name: "demo", Usage: "Show the no-secret first-agent demo command", Description: `Print the provider-free first-agent path for new developers: the deterministic mock-model transcript, when to use it, and where to go next for live-provider chat and inspect/debugging.`, Action: func(c *cli.Context) error { fmt.Fprintln(c.App.Writer, noSecretDemoHelp) return nil }, }, { Name: "quickcheck", Aliases: []string{"debug"}, Usage: "Print first-agent failure-mode quick checks", Description: `Print provider-free recovery breadcrumbs for the scaffold -> run -> chat -> inspect loop, including exact commands for registration, gateway, run history, and no-secret fallback checks.`, Action: func(c *cli.Context) error { fmt.Fprintln(c.App.Writer, firstAgentQuickChecksHelp) return nil }, }, { Name: "preflight", Usage: "Check local prerequisites before the first provider-backed agent", Action: func(c *cli.Context) error { return runAgentPreflight(os.Stdout, defaultPreflightDeps()) }, }, { Name: "doctor", Usage: "Diagnose chat, gateway, registration, provider, and inspect recovery after micro run", Flags: []cli.Flag{ &cli.StringFlag{Name: "gateway", Value: "http://localhost:8080", Usage: "Gateway URL started by micro run"}, }, Action: func(c *cli.Context) error { return runAgentDoctor(os.Stdout, defaultDoctorDeps(), c.String("gateway")) }, }, { Name: "list", Usage: "List registered agents", Action: func(c *cli.Context) error { svcs, err := registry.ListServices() if err != nil { return err } found := false for _, svc := range svcs { records, err := registry.GetService(svc.Name) if err != nil || len(records) == 0 { continue } meta := records[0].Metadata if meta == nil || meta["type"] != "agent" { if len(records[0].Nodes) > 0 { meta = records[0].Nodes[0].Metadata } if meta == nil || meta["type"] != "agent" { continue } } found = true services := meta["services"] if services == "" { services = "(all)" } fmt.Printf(" \033[35m◆\033[0m %-20s manages: %s\n", svc.Name, services) } if !found { fmt.Println(" No agents registered.") fmt.Println() fmt.Println(" Start an agent with:") fmt.Println(" micro run (if agents are part of your project)") } return nil }, }, { Name: "describe", Usage: "Describe an agent", ArgsUsage: "[name]", Action: func(c *cli.Context) error { name := c.Args().First() if name == "" { return fmt.Errorf("usage: micro agent describe [name]") } records, err := registry.GetService(name) if err != nil { return err } if len(records) == 0 { return fmt.Errorf("agent %s not found", name) } b, _ := json.MarshalIndent(records[0], "", " ") fmt.Println(string(b)) return nil }, }, { Name: "resume-input", Usage: "Continue an input-required agent run with human input", ArgsUsage: "[name] [run-id]", Flags: []cli.Flag{ &cli.StringFlag{Name: "input", Usage: "Human input to provide to the paused run", Required: true}, }, Action: func(c *cli.Context) error { name := c.Args().First() runID := c.Args().Get(1) if name == "" || runID == "" { return fmt.Errorf("usage: micro agent resume-input [name] [run-id] --input ") } return resumeInputRun(context.Background(), c.App.Writer, name, runID, c.String("input")) }, }, { Name: "history", Usage: "Show an agent's stored conversation and run history", ArgsUsage: "[name] [run-id]", Flags: runFlags(), Action: func(c *cli.Context) error { name := c.Args().First() if name == "" { return fmt.Errorf("usage: micro agent history [name] [run-id]") } if runID := c.Args().Get(1); runID != "" { return printRunHistory(name, runID, c.Bool("json")) } if c.Bool("json") { return printRunIndex(name, runOptions(c), true) } // Read from the agent's scoped state store (database // "agent", table = name) — available whether or not the // agent is currently running. mem := goagent.NewMemory(store.Scope(store.DefaultStore, "agent", name), "history", 1000) msgs := mem.Messages() if len(msgs) == 0 { fmt.Printf(" No history for agent %q.\n", name) } else { for _, m := range msgs { fmt.Printf(" \033[2m%s:\033[0m %v\n", m.Role, m.Content) } } return printRunIndex(name, runOptions(c), c.Bool("json")) }, }, }, }) } func runFlags() []cli.Flag { return []cli.Flag{ &cli.BoolFlag{Name: "json", Usage: "Print run data as JSON for automation"}, &cli.StringFlag{Name: "status", Usage: "Only show runs with this status (running, done, error, refused)"}, &cli.StringFlag{Name: "trace", Usage: "Only show runs whose trace id matches this full id or prefix"}, &cli.IntFlag{Name: "limit", Usage: "Show the most recently updated N runs"}, } } func runOptions(c *cli.Context) goagent.RunListOptions { return goagent.RunListOptions{Status: c.String("status"), TraceID: c.String("trace"), Limit: c.Int("limit")} } func printRunIndex(name string, opts goagent.RunListOptions, asJSON bool) error { runs, err := goagent.ListRunSummariesWithOptions(store.DefaultStore, name, opts) if err != nil { return err } return writeRunIndex(os.Stdout, name, runs, asJSON) } func writeRunIndex(w io.Writer, name string, runs []goagent.RunSummary, asJSON bool) error { if asJSON { enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(runs) } if len(runs) == 0 { fmt.Fprintf(w, " No runs recorded for agent %q.\n", name) return nil } fmt.Fprintln(w, " Runs:") for _, run := range runs { line := fmt.Sprintf(" %s status=%s events=%d duration=%s last=%s updated=%s", run.RunID, run.Status, run.Events, formatDurationMS(run.DurationMS), run.LastKind, run.UpdatedAt.Format("2006-01-02 15:04:05")) if run.ParentID != "" { line += " parent=" + run.ParentID } if run.TraceID != "" { line += " trace=" + shortTraceID(run.TraceID) } if run.Checkpoint != "" { line += " checkpoint=" + run.Checkpoint } if run.Stage != "" { line += " stage=" + run.Stage } if run.LastError != "" { line += " error=" + run.LastError } fmt.Fprintln(w, line) writeRunIndexBreadcrumbs(w, name, run) } return nil } func writeRunIndexBreadcrumbs(w io.Writer, name string, run goagent.RunSummary) { if run.Stage == "input-required" { fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID) fmt.Fprintf(w, " input: micro agent resume-input %s %s --input \n", name, run.RunID) return } if !isResumableRunSummary(run) { return } fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID) fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID) fmt.Fprintf(w, " stream: call micro.ResumeStreamAsk(ctx, agent, %q) to resume with streaming events\n", run.RunID) } func isResumableRunSummary(run goagent.RunSummary) bool { switch run.Status { case "running", "error", "failed", "refused": return run.Checkpoint != "done" || run.Stage != "" default: return false } } func printRunHistory(name, runID string, asJSON bool) error { events, err := goagent.LoadRunEvents(store.DefaultStore, name, runID) if err != nil { return err } return writeRunHistory(os.Stdout, name, runID, events, asJSON) } func writeRunHistory(w io.Writer, name, runID string, events []goagent.RunEvent, asJSON bool) error { if asJSON { enc := json.NewEncoder(w) enc.SetIndent("", " ") return enc.Encode(events) } if len(events) == 0 { fmt.Fprintf(w, " No run %q for agent %q.\n", runID, name) return nil } for _, e := range events { line := fmt.Sprintf(" %s %-5s", e.Time.Format("15:04:05.000"), e.Kind) if e.Name != "" { line += " " + e.Name } if e.Provider != "" || e.Model != "" { line += fmt.Sprintf(" %s/%s", e.Provider, e.Model) } if e.LatencyMS > 0 { line += fmt.Sprintf(" %dms", e.LatencyMS) } if e.Tokens.TotalTokens > 0 { line += fmt.Sprintf(" tokens=%d", e.Tokens.TotalTokens) } if e.ParentID != "" { line += " parent=" + e.ParentID } if e.TraceID != "" { line += " trace=" + shortTraceID(e.TraceID) } if e.Refused != "" { line += " refused=" + e.Refused } if e.Error != "" { line += " error=" + e.Error } fmt.Fprintln(w, line) } return nil } func formatDurationMS(ms int64) string { if ms <= 0 { return "0ms" } if ms < 1000 { return fmt.Sprintf("%dms", ms) } return fmt.Sprintf("%.1fs", float64(ms)/1000) } func shortTraceID(id string) string { if len(id) <= 12 { return id } return id[:12] } type cliInputPause struct { OriginalMessage string `json:"original_message"` Prompt string `json:"prompt"` } func resumeInputRun(ctx context.Context, w io.Writer, name, runID, input string) error { if input == "" { return fmt.Errorf("input required: pass --input ") } cp := aiflow.StoreCheckpoint(store.DefaultStore, name) run, ok, err := cp.Load(ctx, runID) if err != nil { return err } if !ok { return fmt.Errorf("agent run %s not found for %q", runID, name) } if run.Status != "paused" || run.State.Stage != "input-required" { return fmt.Errorf("agent run %s is not waiting for human input", runID) } var pause cliInputPause _ = run.State.Scan(&pause) reply := "Human input recorded; recreate the agent with the same checkpoint store and call micro.AgentResumeInput to continue model execution." resp := goagent.Response{Reply: reply, Agent: name, RunID: runID, ParentID: run.ParentID} data, err := json.Marshal(resp) if err != nil { return err } run.Status = "done" run.State.Stage = "done" run.State.Data = data for i := range run.Steps { if run.Steps[i].Status == "paused" || run.Steps[i].Name == "ask" { run.Steps[i].Status = "done" run.Steps[i].Error = "" run.Steps[i].Result = "human input: " + input } } if len(run.Steps) == 0 { run.Steps = []aiflow.StepRecord{{Name: "ask", Status: "done", Result: "human input: " + input}} } if err := cp.Save(ctx, run); err != nil { return err } if err := recordCLIResumeEvents(name, runID, run.ParentID); err != nil { return err } if pause.Prompt != "" { fmt.Fprintf(w, " Prompt: %s\n", pause.Prompt) } fmt.Fprintf(w, " Recorded input for agent %q run %s.\n", name, runID) fmt.Fprintf(w, " Inspect: micro inspect agent %s --limit 1\n", name) return nil } func recordCLIResumeEvents(name, runID, parentID string) error { now := time.Now() scoped := store.Scope(store.DefaultStore, "agent", name) events := []goagent.RunEvent{ {Time: now, RunID: runID, ParentID: parentID, Agent: name, Kind: "checkpoint", Name: "done", Status: "done"}, {Time: now.Add(time.Nanosecond), RunID: runID, ParentID: parentID, Agent: name, Kind: "done"}, } for _, e := range events { b, err := json.Marshal(e) if err != nil { return err } key := fmt.Sprintf("runs/%s/%020d-%s", runID, e.Time.UnixNano(), e.Kind) if err := scoped.Write(&store.Record{Key: key, Value: b}); err != nil { return err } } return nil }