Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 665da6c6d2 |
@@ -0,0 +1,169 @@
|
||||
// Package inspect registers the 'micro inspect' CLI command.
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
|
||||
"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/store"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "inspect",
|
||||
Usage: "Inspect recent agent and workflow activity",
|
||||
Description: `Inspect is the CLI checkpoint in the local scaffold → run → chat → inspect loop.
|
||||
It reads durable local run history, so it works after the agent or flow has stopped.`,
|
||||
Subcommands: []*cli.Command{
|
||||
{
|
||||
Name: "agent",
|
||||
Usage: "Show recent recorded runs for an agent",
|
||||
ArgsUsage: "[agent]",
|
||||
Flags: inspectAgentFlags(),
|
||||
Action: inspectAgent,
|
||||
},
|
||||
{
|
||||
Name: "flow",
|
||||
Usage: "Show durable run history for a flow",
|
||||
ArgsUsage: "[flow]",
|
||||
Flags: inspectFlowFlags(),
|
||||
Action: inspectFlow,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
func inspectAgentFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json", Usage: "Print run summaries 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 inspectFlowFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json", Usage: "Print durable run history as JSON for automation"},
|
||||
&cli.BoolFlag{Name: "pending", Usage: "Only show runs that have not completed"},
|
||||
&cli.StringFlag{Name: "status", Usage: "Only show runs with this status (running, done, failed)"},
|
||||
&cli.IntFlag{Name: "limit", Usage: "Show the most recently updated N runs"},
|
||||
&cli.StringFlag{Name: "stage", Usage: "Only show runs currently checkpointed at this stage"},
|
||||
}
|
||||
}
|
||||
|
||||
func inspectAgent(c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
if name == "" {
|
||||
return fmt.Errorf("agent name required: micro inspect agent <name>")
|
||||
}
|
||||
opts := goagent.RunListOptions{Status: c.String("status"), TraceID: c.String("trace"), Limit: c.Int("limit")}
|
||||
runs, err := goagent.ListRunSummariesWithOptions(store.DefaultStore, name, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return writeAgentInspection(os.Stdout, name, runs, c.Bool("json"))
|
||||
}
|
||||
|
||||
func writeAgentInspection(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 agent runs recorded for %q. After chatting, try: micro inspect agent %s\n", name, name)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(w, " Agent %q runs\n", name)
|
||||
for _, run := range runs {
|
||||
fmt.Fprintf(w, " %s status=%s events=%d last=%s", run.RunID, run.Status, run.Events, run.LastKind)
|
||||
if run.LastError != "" {
|
||||
fmt.Fprintf(w, " error=%q", run.LastError)
|
||||
}
|
||||
if run.TraceID != "" {
|
||||
fmt.Fprintf(w, " trace=%s", shortID(run.TraceID))
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func inspectFlow(c *cli.Context) error {
|
||||
name := c.Args().First()
|
||||
if name == "" {
|
||||
return fmt.Errorf("flow name required: micro inspect flow <name>")
|
||||
}
|
||||
runs, err := aiflow.StoreCheckpoint(nil, name).List(context.Background())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
runs = filterFlowInspection(runs, c.Bool("pending"), c.String("status"), c.String("stage"), c.Int("limit"))
|
||||
return writeFlowInspection(os.Stdout, name, runs, c.Bool("json"), c.Bool("pending"))
|
||||
}
|
||||
|
||||
func filterFlowInspection(runs []aiflow.Run, pending bool, status, stage string, limit int) []aiflow.Run {
|
||||
filtered := make([]aiflow.Run, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
if pending && run.Status == "done" {
|
||||
continue
|
||||
}
|
||||
if status != "" && run.Status != status {
|
||||
continue
|
||||
}
|
||||
if stage != "" && run.State.Stage != stage {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, run)
|
||||
}
|
||||
if limit > 0 && len(filtered) > limit {
|
||||
return filtered[len(filtered)-limit:]
|
||||
}
|
||||
return filtered
|
||||
}
|
||||
|
||||
func writeFlowInspection(w io.Writer, name string, runs []aiflow.Run, asJSON, pending bool) error {
|
||||
if asJSON {
|
||||
enc := json.NewEncoder(w)
|
||||
enc.SetIndent("", " ")
|
||||
return enc.Encode(runs)
|
||||
}
|
||||
if len(runs) == 0 {
|
||||
if pending {
|
||||
fmt.Fprintf(w, " No pending flow runs recorded for %q.\n", name)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(w, " No flow runs recorded for %q. After executing a durable flow, try: micro inspect flow %s\n", name, name)
|
||||
return nil
|
||||
}
|
||||
fmt.Fprintf(w, " Flow %q runs\n", name)
|
||||
for _, run := range runs {
|
||||
stage := run.State.Stage
|
||||
if stage == "" {
|
||||
stage = "-"
|
||||
}
|
||||
fmt.Fprintf(w, " %s status=%s stage=%s steps=%d", shortID(run.ID), run.Status, stage, len(run.Steps))
|
||||
for _, step := range run.Steps {
|
||||
if step.Error != "" {
|
||||
fmt.Fprintf(w, " error=%q", step.Error)
|
||||
break
|
||||
}
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func shortID(id string) string {
|
||||
if len(id) <= 12 {
|
||||
return id
|
||||
}
|
||||
return id[:12]
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
package inspect
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
goagent "go-micro.dev/v6/agent"
|
||||
aiflow "go-micro.dev/v6/flow"
|
||||
)
|
||||
|
||||
func TestWriteAgentInspectionIncludesActionableBreadcrumbs(t *testing.T) {
|
||||
runs := []goagent.RunSummary{{RunID: "run-1", Status: "error", Events: 4, LastKind: "tool", LastError: "boom", TraceID: "1234567890abcdef"}}
|
||||
var out bytes.Buffer
|
||||
if err := writeAgentInspection(&out, "support", runs, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=error", "events=4", "last=tool", `error="boom"`, "trace=1234567890ab"} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAgentInspectionEmptyStateNamesInspectCommand(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if err := writeAgentInspection(&out, "support", nil, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := out.String(); !strings.Contains(got, "micro inspect agent support") {
|
||||
t.Fatalf("empty state missing next step: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFlowInspectionIncludesFailedStepBreadcrumb(t *testing.T) {
|
||||
runs := []aiflow.Run{{ID: "1234567890abcdef", Status: "failed", State: aiflow.State{Stage: "charge"}, Steps: []aiflow.StepRecord{{Name: "charge", Status: "failed", Error: "card declined"}}}}
|
||||
var out bytes.Buffer
|
||||
if err := writeFlowInspection(&out, "checkout", runs, false, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"Flow \"checkout\" runs", "1234567890ab", "status=failed", "stage=charge", "steps=1", `error="card declined"`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteFlowInspectionJSON(t *testing.T) {
|
||||
runs := []aiflow.Run{{ID: "run-1", Flow: "checkout", Status: "done"}}
|
||||
var out bytes.Buffer
|
||||
if err := writeFlowInspection(&out, "checkout", runs, true, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var got []aiflow.Run
|
||||
if err := json.Unmarshal(out.Bytes(), &got); err != nil {
|
||||
t.Fatalf("invalid JSON: %v\n%s", err, out.String())
|
||||
}
|
||||
if len(got) != 1 || got[0].ID != "run-1" || got[0].Status != "done" {
|
||||
t.Fatalf("decoded runs = %+v", got)
|
||||
}
|
||||
}
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
_ "go-micro.dev/v6/cmd/micro/cli/build"
|
||||
_ "go-micro.dev/v6/cmd/micro/cli/deploy"
|
||||
_ "go-micro.dev/v6/cmd/micro/flow"
|
||||
_ "go-micro.dev/v6/cmd/micro/inspect"
|
||||
_ "go-micro.dev/v6/cmd/micro/mcp"
|
||||
_ "go-micro.dev/v6/cmd/micro/resource"
|
||||
_ "go-micro.dev/v6/cmd/micro/run"
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestZeroToHeroCLIBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, want := range []string{"run", "chat", "flow"} {
|
||||
for _, want := range []string{"run", "chat", "flow", "inspect"} {
|
||||
if !commands[want] {
|
||||
t.Fatalf("missing %q command", want)
|
||||
}
|
||||
@@ -27,4 +27,7 @@ func TestZeroToHeroCLIBoundaries(t *testing.T) {
|
||||
if !subcommands["flow"]["runs"] {
|
||||
t.Fatal("missing inspect boundary: flow runs")
|
||||
}
|
||||
if !subcommands["inspect"]["agent"] || !subcommands["inspect"]["flow"] {
|
||||
t.Fatal("missing inspect boundary: inspect agent/flow")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,8 +8,9 @@ scripted so CI can run it on every push without external services or model keys.
|
||||
|
||||
1. **Run** — `micro run` remains available as the local development entry point.
|
||||
2. **Chat** — `micro chat` remains available as the interactive agent entry point.
|
||||
3. **Inspect** — `micro flow runs` remains available for durable workflow run
|
||||
history inspection.
|
||||
3. **Inspect** — `micro inspect agent <name>` and `micro inspect flow <name>`
|
||||
remain available as the local run-history inspection step, with `micro flow
|
||||
runs` preserving durable workflow history inspection.
|
||||
|
||||
After the CLI boundary smoke checks, the script runs the deterministic harnesses
|
||||
that boot real services, agents, workflows, store-backed run history, and A2A
|
||||
|
||||
Reference in New Issue
Block a user