Compare commits

..

3 Commits

Author SHA1 Message Date
Codex 1a8eec4dc9 Refresh planner queue after provider inspection
govulncheck / govulncheck (push) Waiting to run
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
2026-07-12 07:06:01 +00:00
Asim Aslam c9e61c0f7b Classify provider failures in agent inspection (#4782)
Co-authored-by: Codex <codex@openai.com>
2026-07-12 06:49:13 +01:00
Asim Aslam 39f8aee34d Refresh planner queue after retry controls (#4778)
Co-authored-by: Codex <codex@openai.com>
2026-07-12 06:13:40 +01:00
7 changed files with 42 additions and 13 deletions
+1 -1
View File
@@ -21,7 +21,7 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Normalize provider error status in agent inspection** ([#4777](https://github.com/micro/go-micro/issues/4777)) — #4775 closed the retry/timeout controls gap, so the next highest-value user-facing gap is operability when a provider still fails: classify timeout, cancellation, rate-limit, auth/configuration, and transient provider errors into stable RunInfo/inspect/tracing details so developers know whether to retry, fix credentials, raise deadlines, or wait out an outage without changing provider public APIs.
1. **Add Gemini provider streaming support** ([#4784](https://github.com/micro/go-micro/issues/4784)) — #4782 closed the provider failure inspection gap, so the next highest-value user-facing gap is the last plain chat streaming hole in provider coverage: implement usable Gemini `ai.Stream` support so `micro chat`, agent streaming, and A2A streaming behave consistently across supported providers, with focused parser/error coverage and no broad public API changes.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+10 -1
View File
@@ -82,7 +82,8 @@ type Usage = ai.Usage
type RunListOptions struct {
// Status, when set, keeps only runs with the matching status
// (for example "running", "done", "canceled", "timeout",
// "rate_limited", "error", or "refused").
// "rate_limited", "auth", "configuration", "unavailable",
// "provider_error", "error", or "refused").
Status string
// TraceID, when set, keeps only runs correlated with this trace id.
// A prefix is accepted so operators can paste the shortened trace id
@@ -662,6 +663,14 @@ func runErrorStatus(kind string) string {
return "timeout"
case ai.ErrorKindRateLimited:
return "rate_limited"
case ai.ErrorKindAuth:
return "auth"
case ai.ErrorKindConfiguration:
return "configuration"
case ai.ErrorKindUnavailable:
return "unavailable"
case ai.ErrorKindProvider:
return "provider_error"
default:
return "error"
}
+4 -1
View File
@@ -663,7 +663,10 @@ func TestRunStatusClassifiesOperationalErrorKinds(t *testing.T) {
{name: "canceled", kind: ai.ErrorKindCanceled, want: "canceled"},
{name: "timeout", kind: ai.ErrorKindTimeout, want: "timeout"},
{name: "rate limited", kind: ai.ErrorKindRateLimited, want: "rate_limited"},
{name: "provider", kind: ai.ErrorKindProvider, want: "error"},
{name: "auth", kind: ai.ErrorKindAuth, want: "auth"},
{name: "configuration", kind: ai.ErrorKindConfiguration, want: "configuration"},
{name: "unavailable", kind: ai.ErrorKindUnavailable, want: "unavailable"},
{name: "provider", kind: ai.ErrorKindProvider, want: "provider_error"},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+16 -6
View File
@@ -85,12 +85,14 @@ func parseRetryAfter(value string, now time.Time) time.Duration {
type ErrorKind string
const (
ErrorKindUnknown ErrorKind = "unknown"
ErrorKindCanceled ErrorKind = "canceled"
ErrorKindTimeout ErrorKind = "timeout"
ErrorKindRateLimited ErrorKind = "rate_limited"
ErrorKindUnavailable ErrorKind = "unavailable"
ErrorKindProvider ErrorKind = "provider"
ErrorKindUnknown ErrorKind = "unknown"
ErrorKindCanceled ErrorKind = "canceled"
ErrorKindTimeout ErrorKind = "timeout"
ErrorKindRateLimited ErrorKind = "rate_limited"
ErrorKindUnavailable ErrorKind = "unavailable"
ErrorKindAuth ErrorKind = "auth"
ErrorKindConfiguration ErrorKind = "configuration"
ErrorKindProvider ErrorKind = "provider"
)
// ClassifiedError is implemented by errors that expose a stable ErrorKind.
@@ -276,6 +278,10 @@ func ClassifyError(err error) ErrorKind {
switch {
case code == 429:
return ErrorKindRateLimited
case code == 401 || code == 403:
return ErrorKindAuth
case code == 400 || code == 404:
return ErrorKindConfiguration
case code >= 500:
return ErrorKindUnavailable
case code > 0:
@@ -288,6 +294,10 @@ func ClassifyError(err error) ErrorKind {
return ErrorKindRateLimited
case strings.Contains(msg, "timeout") || strings.Contains(msg, "deadline"):
return ErrorKindTimeout
case strings.Contains(msg, "unauthorized") || strings.Contains(msg, "forbidden") || strings.Contains(msg, "invalid api key") || strings.Contains(msg, "api key") || strings.Contains(msg, "credential"):
return ErrorKindAuth
case strings.Contains(msg, "missing") || strings.Contains(msg, "not configured") || strings.Contains(msg, "configuration") || strings.Contains(msg, "unsupported model") || strings.Contains(msg, "model not found"):
return ErrorKindConfiguration
case strings.Contains(msg, "temporar") || strings.Contains(msg, "unavailable"):
return ErrorKindUnavailable
default:
+5 -1
View File
@@ -230,9 +230,13 @@ func TestClassifyErrorDistinguishesOperationalOutcomes(t *testing.T) {
{name: "canceled", err: context.Canceled, want: ErrorKindCanceled},
{name: "timeout", err: context.DeadlineExceeded, want: ErrorKindTimeout},
{name: "rate limit status", err: statusErr(429), want: ErrorKindRateLimited},
{name: "auth status", err: statusErr(401), want: ErrorKindAuth},
{name: "configuration status", err: statusErr(400), want: ErrorKindConfiguration},
{name: "unavailable status", err: statusErr(503), want: ErrorKindUnavailable},
{name: "provider status", err: statusErr(400), want: ErrorKindProvider},
{name: "provider status", err: statusErr(409), want: ErrorKindProvider},
{name: "rate limit text", err: errors.New("rate limit exceeded"), want: ErrorKindRateLimited},
{name: "auth text", err: errors.New("invalid API key"), want: ErrorKindAuth},
{name: "configuration text", err: errors.New("model not found"), want: ErrorKindConfiguration},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
+4 -1
View File
@@ -43,7 +43,7 @@ It reads durable local run history, so it works after the agent or flow has stop
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: "status", Usage: "Only show runs with this status (running, done, canceled, timeout, rate_limited, auth, configuration, unavailable, provider_error, 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"},
}
@@ -91,6 +91,9 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
if run.Stage != "" {
fmt.Fprintf(w, " stage=%s", run.Stage)
}
if run.LastErrorKind != "" {
fmt.Fprintf(w, " error_kind=%s", run.LastErrorKind)
}
if run.LastError != "" {
fmt.Fprintf(w, " error=%q", run.LastError)
}
+2 -2
View File
@@ -11,13 +11,13 @@ import (
)
func TestWriteAgentInspectionIncludesActionableBreadcrumbs(t *testing.T) {
runs := []goagent.RunSummary{{RunID: "run-1", Status: "error", Events: 4, LastKind: "tool", LastError: "boom", TraceID: "1234567890abcdef", Checkpoint: "failed", Stage: "ask"}}
runs := []goagent.RunSummary{{RunID: "run-1", Status: "auth", Events: 4, LastKind: "model", LastError: "invalid API key", LastErrorKind: "auth", TraceID: "1234567890abcdef", Checkpoint: "failed", Stage: "ask"}}
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", "checkpoint=failed", "stage=ask", `error="boom"`, "trace=1234567890ab", `micro agent history support run-1`, `micro.AgentResume(ctx, agent, "run-1")`, `micro.ResumeStreamAsk(ctx, agent, "run-1")`} {
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=auth", "events=4", "last=model", "checkpoint=failed", "stage=ask", "error_kind=auth", `error="invalid API key"`, "trace=1234567890ab"} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}