Files
wehub-resource-sync e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:40:33 +08:00

180 lines
7.3 KiB
Go

package agent
import (
"encoding/json"
"fmt"
"io"
"net/http"
"strings"
"time"
goagent "go-micro.dev/v6/agent"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/store"
)
type doctorDeps struct {
getenv func(string) string
httpGet func(string) (*http.Response, error)
listServices func() ([]*registry.Service, error)
getService func(string) ([]*registry.Service, error)
listRuns func(string) ([]goagent.RunSummary, error)
}
func defaultDoctorDeps() doctorDeps {
client := &http.Client{Timeout: 2 * time.Second}
return doctorDeps{
getenv: defaultPreflightDeps().getenv,
httpGet: client.Get,
listServices: registry.ListServices,
getService: registry.GetService,
listRuns: func(name string) ([]goagent.RunSummary, error) {
return goagent.ListRunSummariesWithOptions(store.DefaultStore, name, goagent.RunListOptions{Limit: 1})
},
}
}
func runAgentDoctor(w io.Writer, deps doctorDeps, gateway string) error {
if gateway == "" {
gateway = "http://localhost:8080"
}
gateway = strings.TrimRight(gateway, "/")
checks := agentDoctorChecks(deps, gateway)
failures := 0
fmt.Fprintln(w, "First-agent recovery doctor")
for _, check := range checks {
mark := "✓"
if !check.OK {
mark = "✗"
failures++
}
fmt.Fprintf(w, " %s %s — %s\n", mark, check.Name, check.Detail)
if !check.OK && check.Fix != "" {
fmt.Fprintf(w, " Fix: %s\n", check.Fix)
}
if !check.OK && check.Next != "" {
fmt.Fprintf(w, " Next: %s\n", check.Next)
}
}
if failures > 0 {
return fmt.Errorf("first-agent doctor found %d recovery boundary issue(s)", failures)
}
fmt.Fprintln(w, "\nReady: gateway, agent registration, chat settings, and inspect history are reachable.")
return nil
}
func agentDoctorChecks(deps doctorDeps, gateway string) []preflightCheck {
if deps.getenv == nil {
deps.getenv = defaultPreflightDeps().getenv
}
if deps.httpGet == nil {
deps.httpGet = http.Get
}
if deps.listServices == nil {
deps.listServices = registry.ListServices
}
if deps.getService == nil {
deps.getService = registry.GetService
}
if deps.listRuns == nil {
deps.listRuns = func(name string) ([]goagent.RunSummary, error) {
return goagent.ListRunSummariesWithOptions(store.DefaultStore, name, goagent.RunListOptions{Limit: 1})
}
}
checks := []preflightCheck{checkGateway(deps, gateway), checkChatSettings(deps, gateway)}
agents, regCheck := checkAgentRegistration(deps)
checks = append(checks, regCheck)
checks = append(checks, checkRunHistory(deps, agents))
checks = append(checks, checkProviderConfig(deps))
return checks
}
func checkGateway(deps doctorDeps, gateway string) preflightCheck {
resp, err := deps.httpGet(gateway + "/agent")
if err != nil {
return preflightCheck{Name: "gateway /agent", Detail: err.Error(), Fix: "Start the local gateway with `micro run`, or pass the matching URL with `micro agent doctor --gateway http://localhost:<port>`.", Next: "Then open " + gateway + "/agent or retry `micro chat`."}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return preflightCheck{Name: "gateway /agent", Detail: fmt.Sprintf("%s returned %s", gateway+"/agent", resp.Status), Fix: "Confirm `micro run` is serving the web gateway and that auth/proxy settings are not blocking /agent.", Next: "See docs/guides/debugging-agents.html#chat-and-gateway-failures."}
}
return preflightCheck{Name: "gateway /agent", OK: true, Detail: gateway + "/agent is reachable"}
}
func checkChatSettings(deps doctorDeps, gateway string) preflightCheck {
resp, err := deps.httpGet(gateway + "/api/agent/settings")
if err != nil {
return preflightCheck{Name: "chat settings endpoint", Detail: err.Error(), Fix: "Keep `micro run` running and retry; the playground uses /api/agent/settings before chat prompts.", Next: "See docs/guides/debugging-agents.html#chat-and-gateway-failures."}
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
return preflightCheck{Name: "chat settings endpoint", Detail: fmt.Sprintf("returned %s", resp.Status), Fix: "Check gateway auth/proxy configuration or use the Agent settings page to confirm chat settings load.", Next: "See docs/guides/debugging-agents.html#provider-failures."}
}
var settings map[string]string
_ = json.NewDecoder(resp.Body).Decode(&settings)
if settings["provider"] != "" || settings["model"] != "" || settings["api_key"] != "" {
return preflightCheck{Name: "chat settings endpoint", OK: true, Detail: "reachable with saved provider settings"}
}
return preflightCheck{Name: "chat settings endpoint", OK: true, Detail: "reachable; no saved provider settings"}
}
func checkAgentRegistration(deps doctorDeps) ([]string, preflightCheck) {
services, err := deps.listServices()
if err != nil {
return nil, preflightCheck{Name: "agent registration", Detail: err.Error(), Fix: "Keep the scaffolded agent process running under `micro run` and retry `micro agent list`.", Next: "See docs/guides/your-first-agent.html#run-your-agent."}
}
var agents []string
for _, svc := range services {
records, err := deps.getService(svc.Name)
if err != nil || len(records) == 0 {
continue
}
if serviceIsAgent(records[0]) {
agents = append(agents, svc.Name)
}
}
if len(agents) == 0 {
return nil, preflightCheck{Name: "agent registration", Detail: "no registered agent services found", Fix: "Start an agent project with `micro run` and confirm `micro agent list` shows it.", Next: "Use docs/guides/no-secret-first-agent.html for a deterministic no-provider agent."}
}
return agents, preflightCheck{Name: "agent registration", OK: true, Detail: "found " + strings.Join(agents, ", ")}
}
func serviceIsAgent(svc *registry.Service) bool {
if svc.Metadata != nil && svc.Metadata["type"] == "agent" {
return true
}
for _, node := range svc.Nodes {
if node.Metadata != nil && node.Metadata["type"] == "agent" {
return true
}
}
return false
}
func checkRunHistory(deps doctorDeps, agents []string) preflightCheck {
if len(agents) == 0 {
return preflightCheck{Name: "inspect run history", Detail: "skipped because no agent is registered", Fix: "Fix agent registration first, then chat once and run `micro inspect agent <name>`.", Next: "See docs/guides/debugging-agents.html#inspect-run-history."}
}
for _, name := range agents {
runs, err := deps.listRuns(name)
if err != nil {
return preflightCheck{Name: "inspect run history", Detail: err.Error(), Fix: "Ensure the local store is writable and retry `micro inspect agent " + name + "`.", Next: "See docs/guides/debugging-agents.html#inspect-run-history."}
}
if len(runs) > 0 {
return preflightCheck{Name: "inspect run history", OK: true, Detail: "recent runs available for " + name}
}
}
return preflightCheck{Name: "inspect run history", Detail: "no recorded agent runs yet", Fix: "Send one prompt with `micro chat` or the /agent playground, then run `micro inspect agent " + agents[0] + "`.", Next: "See docs/guides/your-first-agent.html#inspect-what-happened."}
}
func checkProviderConfig(deps doctorDeps) preflightCheck {
check := checkProviderKey(preflightDeps{getenv: deps.getenv})
check.Name = "provider configuration"
if !check.OK {
check.Detail = "no provider key found for live LLM chat"
check.Fix = "For provider-backed chat, export MICRO_AI_API_KEY or a provider-specific key; for no-secret recovery, use the mock-model walkthrough."
}
return check
}