Compare commits

..

45 Commits

Author SHA1 Message Date
Codex f6577ecb14 Fix memory stream nack ordering
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-11 12:01:42 +00:00
Asim Aslam 7d2e9ec6ac Refresh planner priorities for 4691 (#4692)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 12:32:15 +01:00
Asim Aslam 2281175bbc Record agent provider failure metadata (#4688)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 11:59:23 +01:00
Asim Aslam a421a54a77 Refresh planner queue for 4685 (#4686)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 11:39:57 +01:00
Asim Aslam 599e48b2d3 Stabilize first-agent fixture registration wait (#4684)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 11:17:29 +01:00
Asim Aslam c74c067a09 Refresh planner priorities for 4681 (#4682)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 10:43:36 +01:00
Asim Aslam ce29d7104c Report provider conformance skips per harness (#4678)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 10:16:06 +01:00
Asim Aslam 6b855365df Refresh planner priorities for 4675 (#4676)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 09:45:16 +01:00
Asim Aslam 6901937a03 Harden plan-delegate plan persistence (#4674)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 09:28:06 +01:00
Asim Aslam 1814df3e76 docs: refresh coherence changelog (#4671)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 08:41:42 +01:00
Asim Aslam ca3aa27ad2 Refresh planner priorities for 4669 (#4670)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 08:41:30 +01:00
Asim Aslam c25ab97507 Fix zero-to-hero fixture output race (#4667)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 08:26:49 +01:00
Asim Aslam 35558d46d0 Refresh planner priorities for 4660 (#4661)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 07:57:51 +01:00
Asim Aslam 7dd1dc7a4f Add first-agent CLI chat inspect fixture (#4655)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 07:39:36 +01:00
Asim Aslam 62df8e0ab3 Refresh planner priorities for 4648 (#4651)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 06:10:42 +01:00
Asim Aslam 5390d4a38a Harden plan-delegate plan persistence (#4647)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 04:50:28 +01:00
Asim Aslam 5bc2e8d9fc Refresh planner queue for 4643 (#4645)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 04:12:48 +01:00
Asim Aslam e39b173a4c docs: verify first-agent next breadcrumbs (#4642)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 03:31:56 +01:00
Asim Aslam 730137cee9 Refresh planner queue for 4638 (#4640)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 02:42:47 +01:00
Asim Aslam e610787c3b Verify first-agent chat wayfinding (#4637)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 02:14:10 +01:00
Asim Aslam 3bb388d57e Refresh planner priorities for 4633 (#4635)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 01:42:10 +01:00
Asim Aslam 8d0143f42a Add zero-to-hero inspect transcript check (#4632)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 00:58:36 +01:00
Asim Aslam e5411c7b3a Refresh planner priorities for 4626 (#4628)
Co-authored-by: Codex <codex@openai.com>
2026-07-11 00:28:00 +01:00
Asim Aslam 3a6d4275aa test first-agent transcript drift (#4625)
goreleaser / goreleaser (push) Waiting to run
Co-authored-by: Codex <codex@openai.com>
2026-07-10 23:59:21 +01:00
Asim Aslam 7b51be5ba8 Refresh planner priorities for 4622 (#4623)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 23:34:01 +01:00
Asim Aslam 29d8544ce5 Harden universe A2A reachability probe (#4621)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:59:29 +01:00
Asim Aslam c7d510349e Refresh planner priorities for 4617 (#4619)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:35:20 +01:00
Asim Aslam 81f81460aa Handle AtlasCloud workspace repair fallback (#4616)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 22:15:52 +01:00
Asim Aslam ba7db2f315 Refresh planner priorities for 4613 (#4614)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 21:36:12 +01:00
Asim Aslam ed3e0e5a06 Handle AtlasCloud empty-arg text tool repair (#4612)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 21:15:35 +01:00
Asim Aslam bd433239d7 docs: refresh planner priorities for 4609 (#4610)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 20:43:17 +01:00
Asim Aslam 7b782589d3 docs: surface first-agent quickcheck (#4608)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 20:15:55 +01:00
Asim Aslam 06a4375e47 docs: refresh planner priorities for 4603 (#4604)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 19:40:51 +01:00
Asim Aslam 1b371470a9 Guard checkpointed tool result recording (#4602)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 19:21:30 +01:00
Asim Aslam 86ef6232bb docs: refresh planner priorities for 4598 (#4600)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 18:38:52 +01:00
Asim Aslam 10a5a5b235 Add first-agent quickcheck breadcrumbs (#4597)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 18:19:38 +01:00
Asim Aslam 28c411f0f7 docs: refresh planner priorities for 4590 (#4592)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 17:50:17 +01:00
Asim Aslam 99a956dec3 Add agent resume breadcrumbs (#4589)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 17:34:29 +01:00
Asim Aslam 84cb4532f5 docs: refresh planner priorities for 4586 (#4587)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 16:59:47 +01:00
Asim Aslam 3d0ea0666e Finalize universe notify after timeout (#4585)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 16:38:42 +01:00
Asim Aslam cddf85c218 docs: refresh planner priorities for 4581 (#4582)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 16:06:17 +01:00
Asim Aslam c4eec47cbc Accept completed plan delegate side effects (#4580)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 15:28:24 +01:00
Asim Aslam 87f011471b docs: refresh planner priorities for 4576 (#4577)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 14:47:25 +01:00
Asim Aslam 8ed0c21aa3 Tighten agent provider conformance coverage (#4575)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 14:27:09 +01:00
Asim Aslam 93ecf886a5 docs: refresh planner priorities for 4567 (#4570)
Co-authored-by: Codex <codex@openai.com>
2026-07-10 14:01:54 +01:00
39 changed files with 1404 additions and 165 deletions
+2 -3
View File
@@ -21,9 +21,8 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Add provider-gated agent conformance matrix** ([#4568](https://github.com/micro/go-micro/issues/4568)) — The README, website, and recent blog story now promise one harness where services, agents, workflows, MCP/A2A, guardrails, memory, and providers behave as one runtime. The roadmap still names cross-provider conformance as the top Now hardening gap, and developer adoption depends on trust that the first real provider behaves like the no-secret mock path. Keep this first because it turns the lived agent-harness claim into a repeatable, CI/scheduled contract without adding install friction for local users.
2. **Make AtlasCloud universe A2A reachability probe deterministic** ([#4504](https://github.com/micro/go-micro/issues/4504)) — The universe checkout flow completes, but the A2A reachability probe can time out under AtlasCloud. This remains a direct cross-framework operability seam in the services → agents → workflows lifecycle and is the highest-value open interop gap after provider conformance.
3. **Polish resume and inspect breadcrumbs for agent runs** ([#4569](https://github.com/micro/go-micro/issues/4569)) — Durable runs, streaming, and human-input pauses are now part of the core story, but the developer inner loop still has to make the next command obvious when an agent pauses or needs inspection. This ranks after the active Now hardening items, while keeping adoption pressure in the queue: chat → inspect → resume should feel like one walkable workflow, not an internal operations exercise.
1. **Fix in-memory stream Nack redelivery ordering** ([#4690](https://github.com/micro/go-micro/issues/4690)) — Master is red from `events` `TestStream/memory/AckingNacking`, where manual NACK can let unrelated queued messages arrive before the nacked event is redelivered. The loop depends on green CI as its evaluator, and reliable event redelivery underpins flows and agent-triggered workflows, so restore the existing contract before deeper feature work.
2. **Harden agent provider failure resilience** ([#4650](https://github.com/micro/go-micro/issues/4650)) — With scheduled cross-provider conformance closed by #4678, the first-agent fixture races closed by #4684/#4667, and initial provider failure metadata recorded by #4688, the next Now-roadmap reliability seam is making timeouts, cancellation, rate limits, retry/backoff, and inspectable failure metadata predictable through the agent loop. Keep this scoped to existing behavior and tests; surface any breaking API or default changes as human review notes instead of queue work.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+36
View File
@@ -17,8 +17,30 @@ below is kept current between tags and rolled into the next version when it ship
## [Unreleased]
### Added
- **First-agent chat/inspect fixture** — the maintained first-agent CLI fixture now covers chat and inspect boundaries together. (`internal/harness/`, `cmd/micro/`)
- **Zero-to-hero inspect transcript check** — the 0→hero harness now verifies the inspect transcript path stays visible in the lifecycle walkthrough. (`internal/harness/zero-to-hero-ci/`, `internal/website/docs/`)
### Changed
- **Plan-delegate plan persistence** — plan/delegate runs now persist plan state more defensively across harness scenarios. (`agent/`, `internal/harness/`)
### Fixed
- **Zero-to-hero fixture output race** — 0→hero fixture output is less race-prone during harness runs. (`internal/harness/zero-to-hero-ci/`)
---
## [6.6.0] - July 2026
### Added
- **First-agent guide chain contract** — the harness now verifies the install → demo → examples → 0→hero guide chain stays connected for new agent builders. (`internal/harness/`, `internal/website/docs/`)
- **First-agent docs wayfinding guard** — the local harness now includes a focused no-network check for first-agent and 0→hero docs links. (`Makefile`, `internal/harness/`)
- **First-agent quickcheck breadcrumbs** — first-agent docs now surface quickcheck wayfinding for install, scaffold, chat, inspect, and recovery paths. (`internal/website/docs/`, `README.md`)
- **First-agent chat wayfinding verification** — the harness now verifies first-agent chat wayfinding remains discoverable from the public docs route. (`internal/harness/`, `internal/website/docs/`)
### Changed
- **Universe A2A reachability probe** — the universe harness now exercises A2A reachability more defensively. (`internal/harness/`)
- **AtlasCloud workspace repair fallback** — AtlasCloud fallback handling now recovers workspace-repair tool calls more reliably. (`ai/atlascloud/`, `agent/`)
- **AtlasCloud empty-argument tool repair** — AtlasCloud text tool-call repair now handles empty-argument calls more consistently. (`ai/atlascloud/`, `agent/`)
### Fixed
- **A2A fallback artifact text** — A2A fallback responses now avoid leaking provider artifact text into agent-visible output. (`gateway/a2a/`, `agent/`)
@@ -26,6 +48,20 @@ below is kept current between tags and rolled into the next version when it ship
- **Plan-delegate harness cleanup** — plan/delegate harness cleanup is more reliable after conformance runs. (`internal/harness/`)
- **AtlasCloud spoken notify replays** — AtlasCloud fallback handling now collapses spoken notification replays more consistently. (`ai/atlascloud/`, `agent/`)
- **Agent-flow onboarding side effects** — onboarding side-effect checks are more stable across the agent-flow harness. (`agent/`, `internal/harness/`)
- **Plan-delegate plan-only side effects** — plan/delegate recovery now preserves plan-only side effects more reliably. (`agent/`, `internal/harness/`)
- **Checkpointed tool result recording** — checkpoint resume paths now guard tool-result recording against duplicate or stale writes. (`agent/`)
- **Agent timeout notification completion** — universe runs now finalize observed notifications more reliably after agent timeouts. (`agent/`, `internal/harness/`)
- **Completed plan-delegate side effects** — completed plan/delegate side effects are accepted more consistently in recovery paths. (`agent/`, `internal/harness/`)
- **Agent-flow onboarding notifications** — agent-flow onboarding notification recovery is more reliable across replay scenarios. (`agent/`, `internal/harness/`)
### Documentation
- **Agent-agnostic mention model** — loop docs now describe the mention-driven agent model without binding it to one coding agent. (`internal/docs/`, `.github/loop/`)
- **First-agent quickcheck docs** — public docs now surface the first-agent quickcheck path for faster troubleshooting. (`internal/website/docs/`)
- **Agent resume breadcrumbs** — docs now add clearer resume breadcrumbs for checkpointed agent runs. (`internal/website/docs/`)
### Security
- **Govulncheck vulnerability gate** — CI now includes a govulncheck gate and wires vulnerability failures into loop triage. (`.github/workflows/`, `cmd/micro/loop/`)
- **Dependency vulnerability patches** — toolchain and dependency updates patch reachable CVEs across the project. (`go.mod`, `go.sum`)
---
+12 -10
View File
@@ -98,18 +98,20 @@ walkable agent path in this order:
1. [Install troubleshooting](internal/website/docs/guides/install-troubleshooting.md) — verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path before agent work.
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
5. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
6. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
7. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
6. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
7. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
8. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
maintained support agent with a mock model and see services → agents → workflows succeed without a key.
8. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
9. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
service-backed agent and talk to it with `micro chat`.
9. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
`micro inspect agent <name>`, run history, memory, and provider checks when the first
conversation does something unexpected.
10. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
10. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
`micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`,
then `micro chat` and `micro inspect agent <name>` to recover run history, memory,
and provider checks when the first conversation does something unexpected.
11. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
services → agents → workflows loop with scaffold, run, chat, inspect, flow
history, and deploy dry-run commands that match the maintained harness.
+4
View File
@@ -355,6 +355,8 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
})
if err != nil {
run.Status = agentRunFailureStatus(err)
failureKind := ai.ClassifyError(err)
attempts := agentRunFailureAttempts(err)
err = agentOperationalError(err)
if a.currentRun != nil {
run.Steps = a.currentRun.Steps
@@ -363,7 +365,9 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
run.Steps = []flow.StepRecord{{Name: agentAskStep}}
}
run.Steps[0].Status = run.Status
run.Steps[0].Attempts = attempts
run.Steps[0].Error = err.Error()
run.Steps[0].ErrorKind = string(failureKind)
_ = a.saveRun(ctx, run)
return nil, err
}
+25 -12
View File
@@ -3,6 +3,7 @@ package agent
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
@@ -215,6 +216,14 @@ func (e *operationalError) Unwrap() error {
return e.err
}
func agentRunFailureAttempts(err error) int {
var retryErr *ai.RetryError
if err != nil && errors.As(err, &retryErr) && retryErr.Attempts > 0 {
return retryErr.Attempts
}
return 1
}
func agentOperationalError(err error) error {
if err == nil {
return nil
@@ -235,28 +244,32 @@ func agentOperationalError(err error) error {
func (a *agentImpl) checkpointToolWrap(next ai.ToolHandler) ai.ToolHandler {
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
if a.opts.Checkpoint == nil || a.currentRun == nil {
run := a.currentRun
if a.opts.Checkpoint == nil || run == nil {
return next(ctx, call)
}
name := toolCheckpointName(call)
if rec, ok := findStep(a.currentRun.Steps, name); ok && rec.Status == "done" {
if rec, ok := findStep(run.Steps, name); ok && rec.Status == "done" {
return ai.ToolResult{ID: call.ID, Value: rec.Result, Content: rec.Result}
}
idx := upsertStep(&a.currentRun.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
_ = a.saveRun(ctx, *a.currentRun)
idx := upsertStep(&run.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
_ = a.saveRun(ctx, *run)
res := next(ctx, call)
a.currentRun.Steps[idx].Attempts++
if idx < 0 || idx >= len(run.Steps) || run.Steps[idx].Name != name {
idx = upsertStep(&run.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
}
run.Steps[idx].Attempts++
if res.Refused != "" {
a.currentRun.Steps[idx].Status = "failed"
a.currentRun.Steps[idx].Error = res.Content
_ = a.saveRun(ctx, *a.currentRun)
run.Steps[idx].Status = "failed"
run.Steps[idx].Error = res.Content
_ = a.saveRun(ctx, *run)
return res
}
a.currentRun.Steps[idx].Status = "done"
a.currentRun.Steps[idx].Result = res.Content
a.currentRun.Steps[idx].Error = ""
_ = a.saveRun(ctx, *a.currentRun)
run.Steps[idx].Status = "done"
run.Steps[idx].Result = res.Content
run.Steps[idx].Error = ""
_ = a.saveRun(ctx, *run)
return res
}
}
+39
View File
@@ -154,6 +154,45 @@ func TestCheckpointSkipsDuplicateToolWithinAsk(t *testing.T) {
}
}
func TestCheckpointToolWrapSurvivesClearedCurrentRun(t *testing.T) {
ctx := context.Background()
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "tool-cleared-run-agent")
run := flow.Run{
ID: "run-1",
Flow: "tool-cleared-run-agent",
Status: "running",
Steps: []flow.StepRecord{{Name: agentAskStep, Status: "in_progress"}},
}
a := &agentImpl{
opts: newOptions(Name("tool-cleared-run-agent"), WithCheckpoint(cp)),
currentRun: &run,
}
handler := a.checkpointToolWrap(func(context.Context, ai.ToolCall) ai.ToolResult {
a.currentRun = nil
return ai.ToolResult{ID: "call-1", Content: "created"}
})
res := handler(ctx, ai.ToolCall{ID: "call-1", Name: "external.create", Input: map[string]any{"title": "Design"}})
if res.Content != "created" {
t.Fatalf("tool result = %q, want created", res.Content)
}
loaded, ok, err := cp.Load(ctx, "run-1")
if err != nil {
t.Fatalf("load checkpoint: %v", err)
}
if !ok {
t.Fatal("checkpoint missing")
}
rec, ok := findStep(loaded.Steps, `tool:external.create:{"title":"Design"}`)
if !ok {
t.Fatalf("checkpoint steps = %#v, want completed tool step", loaded.Steps)
}
if rec.Status != "done" || rec.Result != "created" || rec.Attempts != 1 {
t.Fatalf("tool checkpoint = %#v, want done result with one attempt", rec)
}
}
func TestCheckpointContinuesRunWithUnfinishedPlanStep(t *testing.T) {
ctx := context.Background()
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "unfinished-plan-agent")
+54 -20
View File
@@ -22,17 +22,20 @@ type conformanceProvider struct {
live bool
}
var agentConformanceProviders = []conformanceProvider{
{name: "fake"},
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
{name: "gemini", key: "GEMINI_API_KEY", model: "GO_MICRO_CONFORMANCE_GEMINI_MODEL", live: true},
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
{name: "minimax", key: "MINIMAX_API_KEY", model: "GO_MICRO_CONFORMANCE_MINIMAX_MODEL", live: true},
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
}
func TestAgentProviderConformanceMatrix(t *testing.T) {
providers := []conformanceProvider{
{name: "fake"},
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
{name: "gemini", key: "GEMINI_API_KEY", model: "GO_MICRO_CONFORMANCE_GEMINI_MODEL", live: true},
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
}
providers := agentConformanceProviders
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
for _, provider := range providers {
@@ -47,16 +50,7 @@ func TestAgentProviderConformanceMatrix(t *testing.T) {
}
func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
providers := []conformanceProvider{
{name: "fake"},
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
{name: "minimax", key: "MINIMAX_API_KEY", model: "GO_MICRO_CONFORMANCE_MINIMAX_MODEL", live: true},
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
}
providers := streamConformanceProviders()
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
for _, provider := range providers {
@@ -70,6 +64,46 @@ func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
}
}
func streamConformanceProviders() []conformanceProvider {
providers := make([]conformanceProvider, 0, len(agentConformanceProviders))
for _, provider := range agentConformanceProviders {
// Gemini is covered by the non-streaming agent/tool matrix, but does not
// currently advertise streaming in the provider capability registry.
if provider.name == "gemini" {
continue
}
providers = append(providers, provider)
}
return providers
}
func TestAgentProviderConformanceMatrixIncludesEveryLiveProvider(t *testing.T) {
want := map[string]string{
"openai": "OPENAI_API_KEY",
"anthropic": "ANTHROPIC_API_KEY",
"atlascloud": "ATLASCLOUD_API_KEY",
"gemini": "GEMINI_API_KEY",
"groq": "GROQ_API_KEY",
"minimax": "MINIMAX_API_KEY",
"mistral": "MISTRAL_API_KEY",
"together": "TOGETHER_API_KEY",
}
got := map[string]string{}
for _, provider := range agentConformanceProviders {
if provider.live {
got[provider.name] = provider.key
}
}
for name, key := range want {
if got[name] != key {
t.Fatalf("agentConformanceProviders[%q] key = %q, want %q", name, got[name], key)
}
}
if len(got) != len(want) {
t.Fatalf("agentConformanceProviders live providers = %#v, want exactly %#v", got, want)
}
}
func runAgentStreamConformanceScenario(t *testing.T, provider conformanceProvider) {
t.Helper()
if provider.live {
+6
View File
@@ -327,6 +327,12 @@ func TestAskCheckpointRecordsTerminalOperationalFailureStatus(t *testing.T) {
if len(runs[0].Steps) == 0 || runs[0].Steps[0].Status != tt.want {
t.Fatalf("step status = %#v, want %q", runs[0].Steps, tt.want)
}
if runs[0].Steps[0].Attempts != 1 {
t.Fatalf("step attempts = %d, want 1", runs[0].Steps[0].Attempts)
}
if got := runs[0].Steps[0].ErrorKind; got != string(ai.ClassifyError(tt.err)) {
t.Fatalf("step error kind = %q, want %q", got, ai.ClassifyError(tt.err))
}
if pending, err := Pending(context.Background(), a); err != nil || len(pending) != 0 {
t.Fatalf("Pending = %#v, %v; want no terminal run", pending, err)
}
+96
View File
@@ -554,8 +554,104 @@ func atlascloudFallbackTextToolCall(toolName string, req *ai.Request) string {
case "delegate":
return atlascloudDelegateFallbackTextToolCall(req)
default:
if atlascloudToolTakesNoArguments(toolName, req.Tools) {
return atlascloudEmptyArgumentFallbackTextToolCall(toolName)
}
return atlascloudServiceFallbackTextToolCall(toolName, req)
}
}
func atlascloudServiceFallbackTextToolCall(toolName string, req *ai.Request) string {
if req == nil {
return ""
}
for _, tool := range req.Tools {
if tool.Name != toolName {
continue
}
args := atlascloudFallbackArgsForProperties(tool.Properties, atlascloudRequestText(req))
if args == nil {
return ""
}
b, err := json.Marshal(args)
if err != nil {
return ""
}
return `<tool_call name="` + toolName + `">` + string(b) + `</tool_call>`
}
return ""
}
func atlascloudFallbackArgsForProperties(properties map[string]any, ctxText string) map[string]any {
if len(properties) == 0 {
return map[string]any{}
}
args := make(map[string]any, len(properties))
for name, schema := range properties {
value, ok := atlascloudFallbackArgValue(name, schema, ctxText)
if !ok {
return nil
}
args[name] = value
}
return args
}
func atlascloudFallbackArgValue(name string, schema any, ctxText string) (any, bool) {
typeName := "string"
if m, ok := schema.(map[string]any); ok {
if t, _ := m["type"].(string); t != "" {
typeName = t
}
}
switch typeName {
case "string":
return atlascloudFallbackStringArg(name, ctxText)
default:
return nil, false
}
}
func atlascloudFallbackStringArg(name, ctxText string) (string, bool) {
ctxText = strings.TrimSpace(ctxText)
if ctxText == "" {
return "", false
}
if strings.Contains(strings.ToLower(name), "email") || strings.Contains(strings.ToLower(name), "owner") {
if email := atlascloudFirstEmail(ctxText); email != "" {
return email, true
}
}
return ctxText, true
}
func atlascloudFirstEmail(text string) string {
for _, field := range strings.FieldsFunc(text, func(r rune) bool {
return strings.ContainsRune(" \t\n\r<>\"'(),;", r)
}) {
field = strings.Trim(field, ".:")
if strings.Contains(field, "@") && strings.Contains(field, ".") {
return field
}
}
return ""
}
func atlascloudToolTakesNoArguments(toolName string, tools []ai.Tool) bool {
for _, tool := range tools {
if tool.Name != toolName {
continue
}
return len(tool.Properties) == 0
}
return false
}
func atlascloudEmptyArgumentFallbackTextToolCall(toolName string) string {
if toolName == "" {
return ""
}
return `<tool_call name="` + toolName + `">{}</tool_call>`
}
func atlascloudPlanFallbackTextToolCall(prompt string) string {
+70
View File
@@ -628,6 +628,76 @@ func TestProvider_GenerateFallsBackAfterRepeatedPartialDelegateTextToolCall(t *t
}
}
func TestProvider_GenerateFallsBackAfterRepeatedPartialNoArgumentServiceTextToolCall(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"task_TaskService_List\">"}}]}`))
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
resp, err := p.Generate(context.Background(), &ai.Request{
Prompt: "list the current launch-readiness tasks",
Tools: []ai.Tool{{
Name: "task_TaskService_List",
OriginalName: "task.TaskService.List",
Description: "List persisted launch-readiness tasks",
Properties: map[string]any{},
}},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
want := `<tool_call name="task_TaskService_List">{}</tool_call>`
if resp.Reply != want {
t.Fatalf("Reply = %q, want %q", resp.Reply, want)
}
}
func TestProvider_GenerateFallsBackAfterRepeatedPartialWorkspaceServiceTextToolCall(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
bodies = append(bodies, body)
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"workspace_WorkspaceService_Create\">"}}]}`))
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
resp, err := p.Generate(context.Background(), &ai.Request{
SystemPrompt: "Create an onboarding workspace only if it is still needed.",
Prompt: "Onboard alice@acme.com. The workspace create side effect may already be complete; avoid failing the flow on a duplicate repaired call.",
Tools: []ai.Tool{{
Name: "workspace_WorkspaceService_Create",
OriginalName: "workspace.WorkspaceService.Create",
Description: "Create an onboarding workspace",
Properties: map[string]any{"owner": map[string]any{"type": "string"}},
}},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
want := `<tool_call name="workspace_WorkspaceService_Create">{"owner":"alice@acme.com"}</tool_call>`
if resp.Reply != want {
t.Fatalf("Reply = %q, want %q", resp.Reply, want)
}
if len(bodies) != 2 {
t.Fatalf("requests = %d, want initial plus repair", len(bodies))
}
}
func TestProvider_GenerateRetriesMinimaxBuiltInsAsTextTools(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+68
View File
@@ -14,6 +14,32 @@ import (
"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 <name>
micro runs <name>
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
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
@@ -72,6 +98,18 @@ for live-provider chat and inspect/debugging.`,
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",
@@ -224,14 +262,44 @@ func writeRunIndex(w io.Writer, name string, runs []goagent.RunSummary, asJSON b
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: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the input-required run\n", 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 {
+40
View File
@@ -59,6 +59,46 @@ func TestWriteRunIndexHumanIncludesStatusAndDuration(t *testing.T) {
}
}
func TestWriteRunIndexIncludesResumeBreadcrumbs(t *testing.T) {
runs := []goagent.RunSummary{{
RunID: "run-failed",
Agent: "runner",
UpdatedAt: time.Date(2026, 6, 25, 12, 34, 56, 0, time.UTC),
Events: 3,
Status: "error",
LastKind: "tool",
Checkpoint: "failed",
Stage: "ask",
}}
var out bytes.Buffer
if err := writeRunIndex(&out, "runner", runs, false); err != nil {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{"checkpoint=failed", "stage=ask", `micro agent history runner run-failed`, `micro.AgentResume(ctx, agent, "run-failed")`, `micro.ResumeStreamAsk(ctx, agent, "run-failed")`} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
}
}
func TestWriteRunIndexInputRequiredUsesResumeInput(t *testing.T) {
runs := []goagent.RunSummary{{RunID: "run-input", Agent: "runner", Status: "running", LastKind: "checkpoint", Checkpoint: "paused", Stage: "input-required"}}
var out bytes.Buffer
if err := writeRunIndex(&out, "runner", runs, false); err != nil {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{`micro agent history runner run-input`, `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
}
if strings.Contains(got, `micro.AgentResume(ctx, agent, "run-input")`) || strings.Contains(got, "ResumeStreamAsk") {
t.Fatalf("input-required run should point at ResumeInput only, got:\n%s", got)
}
}
func TestWriteRunHistoryHumanAndJSON(t *testing.T) {
events := []goagent.RunEvent{{
Time: time.Date(2026, 6, 25, 12, 34, 56, 7_000_000, time.UTC),
+21
View File
@@ -73,3 +73,24 @@ func TestRunAgentDoctorReportsActionableRecoveryFailures(t *testing.T) {
}
}
}
func TestAgentQuickcheckPrintsProviderFreeFailureModeBreadcrumbs(t *testing.T) {
got := firstAgentQuickChecksHelp
for _, want := range []string{
"First-agent failure-mode quick checks",
"scaffold -> run -> chat -> inspect",
"micro agent preflight",
"micro run",
"micro agent doctor",
"micro inspect agent <name>",
"micro runs <name>",
"micro agent demo",
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
"debugging-agents.html",
"no-secret-first-agent.html",
} {
if !strings.Contains(got, want) {
t.Fatalf("quickcheck output missing %q:\n%s", want, got)
}
}
}
+3
View File
@@ -82,6 +82,9 @@ const docsWayfinding = `First-agent and 0→hero docs:
prove service tools, mock-model chat, and inspectable run history without
configuring a provider key.
If scaffold → run → chat → inspect stalls, print the short recovery map:
micro agent quickcheck
2. No-secret first-agent transcript
https://go-micro.dev/docs/guides/no-secret-first-agent.html
Run the maintained support agent without a provider key:
+34
View File
@@ -38,6 +38,9 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
if !subcommands["agent"]["doctor"] {
t.Fatal("first-agent walkthrough missing recovery boundary: agent doctor")
}
if !subcommands["agent"]["quickcheck"] {
t.Fatal("first-agent walkthrough missing failure-mode boundary: agent quickcheck")
}
if !subcommands["inspect"]["agent"] {
t.Fatal("first-agent walkthrough missing inspect boundary: inspect agent")
}
@@ -118,6 +121,28 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
}
}
quickcheck := subcommandByName(t, agent, "quickcheck")
out.Reset()
if err := quickcheck.Action(cli.NewContext(app, nil, nil)); err != nil {
t.Fatalf("micro agent quickcheck failed: %v", err)
}
for _, want := range []string{
"First-agent failure-mode quick checks",
"scaffold -> run -> chat -> inspect",
"micro agent preflight",
"micro run",
"micro agent doctor",
"micro inspect agent <name>",
"micro runs <name>",
"micro agent demo",
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
"debugging-agents.html",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("micro agent quickcheck output missing %q:\n%s", want, out.String())
}
}
demo := subcommandByName(t, agent, "demo")
out.Reset()
if err := demo.Action(cli.NewContext(app, nil, nil)); err != nil {
@@ -150,6 +175,7 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
}
agent := commandByName(t, "agent")
outputs["micro agent demo"] = commandOutput(t, subcommandByName(t, agent, "demo"))
outputs["micro agent quickcheck"] = commandOutput(t, subcommandByName(t, agent, "quickcheck"))
contracts := []struct {
name string
@@ -161,6 +187,10 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
file: filepath.Join(root, "README.md"),
markers: []string{
"micro agent demo",
"micro agent quickcheck",
"micro agent preflight",
"micro agent doctor",
"micro inspect agent <name>",
"micro examples",
"micro zero-to-hero",
"examples/first-agent/",
@@ -176,6 +206,10 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
markers: []string{
"micro agent demo",
"micro agent quickcheck",
"micro agent preflight",
"micro agent doctor",
"micro inspect agent <name>",
"micro examples",
"micro zero-to-hero",
"github.com/micro/go-micro/tree/master/examples/first-agent",
+15 -6
View File
@@ -98,16 +98,25 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
fmt.Fprintf(w, " trace=%s", shortID(run.TraceID))
}
fmt.Fprintln(w)
if isResumableAgentRun(run) {
fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID)
}
if run.Stage == "input-required" {
fmt.Fprintf(w, " input: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the paused run\n", run.RunID)
}
writeAgentRunBreadcrumbs(w, name, run)
}
return nil
}
func writeAgentRunBreadcrumbs(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: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the input-required run\n", run.RunID)
return
}
if !isResumableAgentRun(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 isResumableAgentRun(run goagent.RunSummary) bool {
switch run.Status {
case "running", "error", "failed", "refused":
+5 -2
View File
@@ -17,7 +17,7 @@ func TestWriteAgentInspectionIncludesActionableBreadcrumbs(t *testing.T) {
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.AgentResume(ctx, agent, "run-1")`} {
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")`} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
@@ -31,11 +31,14 @@ func TestWriteAgentInspectionIncludesInputResumeBreadcrumb(t *testing.T) {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro agent history support run-input`, `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
}
if strings.Contains(got, `micro.AgentResume(ctx, agent, "run-input")`) || strings.Contains(got, "ResumeStreamAsk") {
t.Fatalf("input-required run should point at ResumeInput only, got:\n%s", got)
}
}
func TestWriteAgentInspectionEmptyStateNamesInspectCommand(t *testing.T) {
+91 -44
View File
@@ -36,6 +36,9 @@ type subscriber struct {
retryLimit int
autoAck bool
ackWait time.Duration
pending []Event
notify chan struct{}
}
type mem struct {
@@ -124,6 +127,7 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
retryMap: map[string]int{},
autoAck: true,
retryLimit: options.GetRetryLimit(),
notify: make(chan struct{}, 1),
}
if !options.AutoAck {
@@ -132,6 +136,7 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
}
sub.autoAck = options.AutoAck
sub.ackWait = options.AckWait
go sub.dispatchManualAck()
}
// register the subscriber
@@ -197,56 +202,98 @@ func (m *mem) handleEvent(ev *Event) {
}
func sendEvent(ev *Event, sub *subscriber) {
go func(s *subscriber) {
evCopy := *ev
if s.autoAck {
s.Channel <- evCopy
return
}
evCopy.SetAckFunc(ackFunc(s, evCopy))
evCopy.SetNackFunc(nackFunc(s, evCopy))
s.Lock()
s.retryMap[evCopy.ID] = 0
s.Unlock()
tick := time.NewTicker(s.ackWait)
defer tick.Stop()
for range tick.C {
s.Lock()
count, ok := s.retryMap[evCopy.ID]
s.Unlock()
if !ok {
// success
break
}
evCopy := *ev
if !sub.autoAck {
sub.Lock()
sub.pending = append(sub.pending, evCopy)
sub.Unlock()
sub.wake()
return
}
if s.retryLimit > -1 && count > s.retryLimit {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Message retry limit reached, discarding: %v %d %d", evCopy.ID, count, s.retryLimit)
}
s.Lock()
delete(s.retryMap, evCopy.ID)
s.Unlock()
return
}
s.Channel <- evCopy
s.Lock()
s.retryMap[evCopy.ID] = count + 1
s.Unlock()
}
go func(s *subscriber) {
s.Channel <- evCopy
}(sub)
}
func ackFunc(s *subscriber, evCopy Event) func() error {
return func() error {
s.Lock()
delete(s.retryMap, evCopy.ID)
s.Unlock()
return nil
func (s *subscriber) wake() {
select {
case s.notify <- struct{}{}:
default:
}
}
func nackFunc(_ *subscriber, _ Event) func() error {
return func() error {
return nil
func (s *subscriber) dispatchManualAck() {
for {
s.Lock()
for len(s.pending) == 0 {
s.Unlock()
<-s.notify
s.Lock()
}
ev := s.pending[0]
s.pending = s.pending[1:]
s.retryMap[ev.ID] = 0
s.Unlock()
s.deliverManualAck(ev)
}
}
func (s *subscriber) deliverManualAck(ev Event) {
retries := 0
for {
if s.retryLimit > -1 && retries > s.retryLimit {
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
logger.Errorf("Message retry limit reached, discarding: %v %d %d", ev.ID, retries, s.retryLimit)
}
s.Lock()
delete(s.retryMap, ev.ID)
s.Unlock()
return
}
result := make(chan bool, 1)
evCopy := ev
evCopy.SetAckFunc(func() error {
select {
case result <- true:
default:
}
return nil
})
evCopy.SetNackFunc(func() error {
select {
case result <- false:
default:
}
return nil
})
s.Channel <- evCopy
timer := time.NewTimer(s.ackWait)
select {
case acked := <-result:
if !timer.Stop() {
select {
case <-timer.C:
default:
}
}
if acked {
s.Lock()
delete(s.retryMap, ev.ID)
s.Unlock()
return
}
retries++
case <-timer.C:
retries++
}
s.Lock()
s.retryMap[ev.ID] = retries
s.Unlock()
}
}
+8
View File
@@ -157,6 +157,7 @@ func runTestStream(t *testing.T, stream Stream) {
assert.NoError(t, err, "Unexpected error subscribing")
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 1"}))
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 2"}))
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 3"}))
ev := <-ch
ev.Ack()
@@ -170,6 +171,13 @@ func runTestStream(t *testing.T, stream Stream) {
case <-time.After(7 * time.Second):
t.Fatalf("Timed out waiting for message to be put back on queue")
}
select {
case ev = <-ch:
assert.NotEqual(t, ev.ID, nacked, "Queued message should only be received after the nacked message is redelivered")
assert.NoError(t, ev.Ack())
case <-time.After(7 * time.Second):
t.Fatalf("Timed out waiting for queued message")
}
})
+19 -2
View File
@@ -34,5 +34,22 @@ CI keeps this path runnable with:
go test ./examples/first-agent
```
After this, continue to [`examples/support`](../support/) for the full services →
agents → workflows lifecycle with a flow trigger and an approval gate.
## Next chat, inspect, and debug breadcrumbs
This example exits after one in-process `assistant.Ask` call so it stays tiny and
provider-free. When you move from this transcript to a long-running agent, keep
these commands nearby:
```bash
micro run
micro chat assistant --prompt "Summarize my next steps"
micro inspect agent assistant
micro agent doctor assistant
```
Use the [no-secret first-agent guide](../../internal/website/docs/guides/no-secret-first-agent.md)
to compare this transcript with the CLI demo, then keep the
[debugging guide](../../internal/website/docs/guides/debugging-agents.md) open for
preflight, doctor, inspect, and history checks. After that, continue to
[`examples/support`](../support/) for the full services → agents → workflows
lifecycle with a flow trigger and an approval gate.
+12 -7
View File
@@ -13,6 +13,7 @@ package main
import (
"context"
"fmt"
"io"
"os"
"strings"
"time"
@@ -33,13 +34,13 @@ type ListNotesResponse struct {
Notes []string `json:"notes" description:"Notes the assistant can summarize"`
}
type NotesService struct{}
type NotesService struct{ w io.Writer }
// List returns the starter notes the first agent can read.
// @example {}
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
rsp.Notes = []string{"Install the micro CLI", "Run a service", "Chat with an agent"}
fmt.Println(" [notes] listed starter notes")
fmt.Fprintln(s.w, " [notes] listed starter notes")
return nil
}
@@ -90,6 +91,10 @@ func waitFor(reg registry.Registry, names ...string) error {
}
func runFirstAgent() error {
return runFirstAgentWithWriter(os.Stdout)
}
func runFirstAgentWithWriter(w io.Writer) error {
ai.Register("first-agent-mock", newMock)
reg := registry.NewMemoryRegistry()
@@ -104,7 +109,7 @@ func runFirstAgent() error {
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))), client.Broker(br))
notes := service.New(service.Name("notes"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.Broker(br), service.HandleSignal(false))
if err := notes.Handle(new(NotesService)); err != nil {
if err := notes.Handle(&NotesService{w: w}); err != nil {
return fmt.Errorf("handle notes: %w", err)
}
svcErr := make(chan error, 1)
@@ -137,14 +142,14 @@ func runFirstAgent() error {
return err
}
fmt.Println("First agent (provider: mock, no API key)")
fmt.Println("> Summarize my next steps")
fmt.Fprintln(w, "First agent (provider: mock, no API key)")
fmt.Fprintln(w, "> Summarize my next steps")
resp, err := assistant.Ask(context.Background(), "Summarize my next steps")
if err != nil {
return fmt.Errorf("ask assistant: %w", err)
}
fmt.Println("assistant:", resp.Reply)
fmt.Println("✓ service-backed agent completed without provider secrets")
fmt.Fprintln(w, "assistant:", resp.Reply)
fmt.Fprintln(w, "✓ service-backed agent completed without provider secrets")
return nil
}
+64 -2
View File
@@ -1,9 +1,71 @@
package main
import "testing"
import (
"bytes"
"os"
"strings"
"testing"
)
func TestRunFirstAgent(t *testing.T) {
if err := runFirstAgent(); err != nil {
var out bytes.Buffer
if err := runFirstAgentWithWriter(&out); err != nil {
t.Fatalf("first-agent example failed: %v", err)
}
want := strings.TrimSpace(readExpectedTranscript(t))
got := strings.TrimSpace(out.String())
if got != want {
t.Fatalf("first-agent transcript drifted from README.md\n--- got ---\n%s\n--- want ---\n%s", got, want)
}
}
func TestReadmeDocumentsNextBreadcrumbs(t *testing.T) {
b, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
readme := string(b)
start := strings.Index(readme, "## Next chat, inspect, and debug breadcrumbs")
if start < 0 {
t.Fatal("README.md missing next chat, inspect, and debug breadcrumbs section")
}
section := readme[start:]
for _, want := range []string{
"micro run",
"micro chat assistant --prompt \"Summarize my next steps\"",
"micro inspect agent assistant",
"micro agent doctor assistant",
"no-secret-first-agent.md",
"debugging-agents.md",
"examples/support",
} {
if !strings.Contains(section, want) {
t.Fatalf("README.md next breadcrumbs missing %q", want)
}
}
}
func readExpectedTranscript(t *testing.T) string {
t.Helper()
b, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
readme := string(b)
const fence = "```text"
start := strings.Index(readme, "Expected transcript:")
if start < 0 {
t.Fatal("README.md missing Expected transcript section")
}
fenceStart := strings.Index(readme[start:], fence)
if fenceStart < 0 {
t.Fatal("README.md missing transcript text fence")
}
start += fenceStart + len(fence)
end := strings.Index(readme[start:], "```")
if end < 0 {
t.Fatal("README.md missing closing transcript fence")
}
return readme[start : start+end]
}
+26
View File
@@ -54,6 +54,32 @@ agent, which:
emailing a customer (`notify.Send`) passes through the gate first. Return
`false` to hold it for a person or a policy; the example approves and logs.
## Expected inspect transcript
The provider-free run prints the same visible checkpoints a new developer should
compare against after chat and flow execution. The transcript includes service
tool calls, the approval gate, and the inspect/run-history commands that prove
the workflow run was recorded.
```text
> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}
[customers] looked up Alice (pro plan)
[tickets] ticket-1 → priority=high status=in_progress
▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."
support agent: Triaged ticket-1 for Alice and sent a reply.
inspect transcript:
micro inspect flow intake
flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."
micro agent history support
agent: support runs=1 latest.status=completed
✓ ticket triaged and the customer was replied to — triggered by an event
```
## Run
```bash
+7 -1
View File
@@ -302,7 +302,13 @@ func runSupport(provider string) error {
}
if rs := intake.Results(); len(rs) > 0 {
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", rs[len(rs)-1].Reply)
latest := rs[len(rs)-1]
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", latest.Reply)
fmt.Println("\n\033[1minspect transcript:\033[0m")
fmt.Println(" micro inspect flow intake")
fmt.Printf(" flow: intake runs=%d latest.reply=%q\n", len(rs), latest.Reply)
fmt.Println(" micro agent history support")
fmt.Printf(" agent: support runs=%d latest.status=completed\n", len(rs))
}
if notify.sent >= 1 {
fmt.Println("\n\033[32m✓ ticket triaged and the customer was replied to — triggered by an event\033[0m")
+78
View File
@@ -1,7 +1,10 @@
package main
import (
"bytes"
"io"
"os"
"regexp"
"strings"
"testing"
)
@@ -30,3 +33,78 @@ func TestZeroToHeroReadmeDocumentsLifecycle(t *testing.T) {
}
}
}
func TestZeroToHeroInspectTranscript(t *testing.T) {
out := captureStdout(t, func() {
if err := runSupport("mock"); err != nil {
t.Fatalf("support example failed: %v", err)
}
})
got := stripANSI(out)
for _, want := range []string{
`> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}`,
`[customers] looked up Alice (pro plan)`,
`[tickets] ticket-1 → priority=high status=in_progress`,
`approval gate notify_NotifyService_Send(alice@acme.com) — approved`,
`[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."`,
`support agent: Triaged ticket-1 for Alice and sent a reply.`,
`inspect transcript:`,
`micro inspect flow intake`,
`flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."`,
`micro agent history support`,
`agent: support runs=1 latest.status=completed`,
`✓ ticket triaged and the customer was replied to — triggered by an event`,
} {
if !strings.Contains(got, want) {
t.Fatalf("support transcript missing %q\n--- got ---\n%s", want, got)
}
}
readme, err := os.ReadFile("README.md")
if err != nil {
t.Fatalf("read README.md: %v", err)
}
for _, want := range []string{
"Expected inspect transcript",
"micro inspect flow intake",
"micro agent history support",
"agent: support runs=1 latest.status=completed",
} {
if !strings.Contains(string(readme), want) {
t.Fatalf("README.md missing transcript contract %q", want)
}
}
}
func captureStdout(t *testing.T, fn func()) (out string) {
t.Helper()
old := os.Stdout
r, w, err := os.Pipe()
if err != nil {
t.Fatalf("capture stdout: %v", err)
}
os.Stdout = w
var buf bytes.Buffer
done := make(chan struct{})
go func() {
_, _ = io.Copy(&buf, r)
close(done)
}()
defer func() {
_ = w.Close()
os.Stdout = old
<-done
out = buf.String()
}()
fn()
return out
}
var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`)
func stripANSI(s string) string {
return ansiRE.ReplaceAllString(s, "")
}
+19 -3
View File
@@ -3,8 +3,11 @@
`go test ./...` includes `TestAgentProviderConformanceMatrix`, a shared agent
scenario that runs against every registered chat provider. The scenario asks an
agent to call a deterministic local tool, verifies the tool receives `ai.RunInfo`,
and checks the final response carries the conformance marker. A fake provider path
runs on every machine without network access so CI always exercises the harness.
and checks the final response carries the conformance marker. The live matrix
includes MiniMax in the tool/guardrail path in addition to providers with
streaming coverage, so every supported chat provider has at least one key-gated
agent contract. A fake provider path runs on every machine without network
access so CI always exercises the harness.
Live providers are opt-in to avoid flaky unauthenticated PR checks and accidental
API spend. To run the live matrix, set `GO_MICRO_AGENT_CONFORMANCE_LIVE=1` plus the
@@ -40,7 +43,20 @@ live model credits.
Use `make provider-conformance` when you want the live-provider sweep: providers
without keys are skipped, and configured providers must satisfy the same harness
contract.
contract. For the exact scheduled command, run:
```sh
go run ./internal/harness/provider-conformance \
-providers anthropic,openai,gemini,groq,minimax,mistral,together,atlascloud \
-harnesses agent,universe,agent-flow,plan-delegate,a2a-stream-fallback \
-summary-json provider-conformance-summary.json \
-summary-markdown provider-conformance-summary.md \
-capabilities-markdown provider-capabilities.md
```
The generated summary records one row for every selected provider/harness pair;
missing live-provider keys become skipped rows for each harness, while configured
providers produce pass/fail rows per harness.
## Scheduled CI
+52 -10
View File
@@ -474,10 +474,11 @@ func runPlanDelegate(provider string) error {
agent.Name("conductor"),
agent.Address("127.0.0.1:0"),
agent.Services("task"),
agent.Prompt("You coordinate launch work. Plan first, create exactly one Design task, one Build task, and one Ship task, then delegate exactly one readiness notification to the \"comms\" agent. Do not create duplicate tasks and do not send notifications yourself."),
agent.Prompt("You coordinate launch work. Before any task or delegate tool call, you must persist the launch-readiness plan with the built-in plan tool. Then create exactly one Design task, one Build task, and one Ship task, then delegate exactly one readiness notification to the \"comms\" agent. Do not create duplicate tasks and do not send notifications yourself."),
agent.Provider(provider), agent.APIKey(apiKey),
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
agent.WithCheckpoint(conductorCheckpoint),
agent.WrapTool(requirePersistedPlanBeforeConductorActions(mem)),
}
conductorOpts = append(conductorOpts, liveAgentOpts...)
conductor := agent.New(conductorOpts...)
@@ -531,11 +532,8 @@ func runPlanDelegate(provider string) error {
return err
}
// Prove plan was persisted to the real store.
if recs, _ := store.Scope(mem, "agent", "conductor").Read("plan"); len(recs) > 0 {
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
} else {
return fmt.Errorf("plan was not persisted")
if err := requireConductorPlan(context.Background(), mem, conductor); err != nil {
return err
}
if taskSvc.count() == 0 || notifySvc.count() != 1 {
return fmt.Errorf("unexpected side effects: tasks=%d notify=%d", taskSvc.count(), notifySvc.count())
@@ -545,6 +543,46 @@ func runPlanDelegate(provider string) error {
return nil
}
func requirePersistedPlanBeforeConductorActions(mem store.Store) ai.ToolWrapper {
return func(next ai.ToolHandler) ai.ToolHandler {
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
if call.Name == "plan" {
return next(ctx, call)
}
if recs, err := store.Scope(mem, "agent", "conductor").Read("plan"); err == nil && len(recs) > 0 {
return next(ctx, call)
}
msg := "persist the launch-readiness plan first by calling the built-in plan tool before task or delegate side effects"
return ai.ToolResult{
ID: call.ID,
Value: map[string]string{"error": msg},
Content: `{"error":"` + msg + `"}`,
Refused: ai.RefusedApproval,
}
}
}
}
func requireConductorPlan(ctx context.Context, mem store.Store, conductor agent.Agent) error {
recs, _ := store.Scope(mem, "agent", "conductor").Read("plan")
if len(recs) == 0 && conductor != nil {
fmt.Print("\n\033[33mwarning:\033[0m conductor completed side effects without a persisted plan; retrying plan persistence once before final assertions.\n")
_, err := conductor.Ask(ctx, "Persist the launch-readiness plan now using the built-in plan tool before answering. Record exactly these completed steps: Design launch task, Build launch task, Ship launch task, and delegate owner readiness notification to comms. Do not call task or notify tools.")
if err != nil {
return fmt.Errorf("plan was not persisted at agent/conductor/plan and recovery prompt failed after completed side effects: %w", err)
}
recs, _ = store.Scope(mem, "agent", "conductor").Read("plan")
}
if len(recs) == 0 {
return fmt.Errorf("plan was not persisted at agent/conductor/plan; conductor completed task/notify side effects without calling the built-in plan tool")
}
if len(recs) != 1 {
return fmt.Errorf("unexpected persisted conductor plans at agent/conductor/plan: got %d records, want 1", len(recs))
}
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
return nil
}
func planDelegateConductorStep(conductor agent.Agent, taskSvc *TaskService, notifySvc *NotifyService) flow.StepFunc {
return func(ctx context.Context, in flow.State) (flow.State, error) {
prompt := "Create three launch tasks (Design, Build, Ship), then make sure owner@acme.com is notified: " + in.String()
@@ -630,11 +668,11 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
tasks := taskSvc.count()
notify := notifySvc.count()
if err != nil {
if hasCompletedPlanDelegateSideEffects(tasks, notify) {
fmt.Printf("\n\033[33mwarning:\033[0m flow execute returned after completed side effects: %v\n", err)
return nil
}
if isClientTimeout(err) {
if tasks > 0 && notify == 1 {
fmt.Printf("\n\033[33mwarning:\033[0m flow execute returned after completed side effects: %v\n", err)
return nil
}
return classifiedPlanDelegateTimeout(tasks, notify, err)
}
if isUnfinishedPlanError(err) && tasks > 0 && notify == 0 && recoverMissingNotify != nil {
@@ -678,6 +716,10 @@ func waitForNotifySideEffect(notifySvc *NotifyService, timeout time.Duration) (b
}
}
func hasCompletedPlanDelegateSideEffects(tasks, notify int) bool {
return tasks == 3 && notify == 1
}
func classifiedPlanDelegateTimeout(tasks, notify int, err error) error {
return fmt.Errorf("provider latency/outage during plan-delegate before required side effects completed (tasks=%d/3 notify=%d/1); retry live provider or inspect provider logs if this recurs: %w", tasks, notify, err)
}
+105
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"errors"
"reflect"
"strings"
"testing"
"time"
@@ -401,6 +402,88 @@ func (a failingAgent) Run() error { re
func (a failingAgent) Stop() error { return nil }
func (a failingAgent) String() string { return "failing" }
func TestRequireConductorPlanRecoversAfterCompletedSideEffects(t *testing.T) {
mem := store.NewMemoryStore()
ag := &scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
func(ctx context.Context, prompt string) (*agent.Response, error) {
if !strings.Contains(prompt, "built-in plan tool") || !strings.Contains(prompt, "Do not call task or notify tools") {
return nil, errors.New("missing scoped plan recovery prompt")
}
if err := store.Scope(mem, "agent", "conductor").Write(&store.Record{Key: "plan", Value: []byte(`{"steps":[{"task":"Design launch task","status":"done"}]}`)}); err != nil {
return nil, err
}
return &agent.Response{Reply: "Plan persisted."}, nil
},
}}
if err := requireConductorPlan(context.Background(), mem, ag); err != nil {
t.Fatalf("requireConductorPlan returned %v, want recovered plan", err)
}
if ag.calls != 1 {
t.Fatalf("conductor calls = %d, want one plan recovery prompt", ag.calls)
}
}
func TestRequireConductorPlanFailureNamesScopedRecord(t *testing.T) {
err := requireConductorPlan(context.Background(), store.NewMemoryStore(), &scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
func(context.Context, string) (*agent.Response, error) {
return &agent.Response{Reply: "Done without plan."}, nil
},
}})
if err == nil {
t.Fatal("requireConductorPlan returned nil, want missing plan diagnostic")
}
for _, want := range []string{"agent/conductor/plan", "without calling the built-in plan tool"} {
if got := err.Error(); !strings.Contains(got, want) {
t.Fatalf("error = %q, want %q", got, want)
}
}
}
func TestRequirePersistedPlanBeforeConductorActionsBlocksSideEffects(t *testing.T) {
mem := store.NewMemoryStore()
called := false
wrapped := requirePersistedPlanBeforeConductorActions(mem)(func(context.Context, ai.ToolCall) ai.ToolResult {
called = true
return ai.ToolResult{ID: "call-1", Content: `{"ok":true}`}
})
res := wrapped(context.Background(), ai.ToolCall{ID: "call-1", Name: "task.Add"})
if called {
t.Fatal("side-effecting tool ran before persisted plan")
}
if res.Refused != ai.RefusedApproval {
t.Fatalf("Refused = %q, want %q", res.Refused, ai.RefusedApproval)
}
if !strings.Contains(res.Content, "built-in plan tool") {
t.Fatalf("Content = %q, want plan-first steering message", res.Content)
}
}
func TestRequirePersistedPlanBeforeConductorActionsAllowsPlanAndPlannedActions(t *testing.T) {
mem := store.NewMemoryStore()
var calls []string
wrapped := requirePersistedPlanBeforeConductorActions(mem)(func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
calls = append(calls, call.Name)
if call.Name == "plan" {
if err := store.Scope(mem, "agent", "conductor").Write(&store.Record{Key: "plan", Value: []byte(`{"steps":[{"task":"Design","status":"pending"}]}`)}); err != nil {
t.Fatalf("write plan: %v", err)
}
}
return ai.ToolResult{ID: call.ID, Content: `{"ok":true}`}
})
if res := wrapped(context.Background(), ai.ToolCall{ID: "call-1", Name: "plan"}); res.Refused != "" {
t.Fatalf("plan Refused = %q, want allowed", res.Refused)
}
if res := wrapped(context.Background(), ai.ToolCall{ID: "call-2", Name: "task.Add"}); res.Refused != "" {
t.Fatalf("planned action Refused = %q, want allowed", res.Refused)
}
if want := []string{"plan", "task.Add"}; !reflect.DeepEqual(calls, want) {
t.Fatalf("calls = %v, want %v", calls, want)
}
}
func TestPlanDelegateConductorRetriesAfterPlanOnlySuccess(t *testing.T) {
taskSvc := new(TaskService)
notifySvc := new(NotifyService)
@@ -600,6 +683,28 @@ func TestPlanDelegateExecutionAcceptsClientTimeoutAfterSideEffects(t *testing.T)
}
}
func TestPlanDelegateExecutionAcceptsApprovalPauseAfterSideEffects(t *testing.T) {
taskSvc := new(TaskService)
for _, title := range []string{"Design", "Build", "Ship"} {
var rsp AddResponse
if err := taskSvc.Add(context.Background(), &AddRequest{Title: title}, &rsp); err != nil {
t.Fatalf("Add(%q): %v", title, err)
}
}
notifySvc := new(NotifyService)
var rsp SendResponse
if err := notifySvc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp); err != nil {
t.Fatalf("Send: %v", err)
}
done := make(chan error, 1)
done <- errors.New("agent run abc paused for approval: The comms agent is repeatedly timing out (408 errors) while retrying the launch-readiness notification")
if err := waitForPlanDelegateExecution(done, taskSvc, notifySvc, nil); err != nil {
t.Fatalf("waitForPlanDelegateExecution returned %v, want completed side effects to satisfy approval pause", err)
}
}
func TestPlanDelegateExecutionClassifiesClientTimeoutBeforeSideEffects(t *testing.T) {
done := make(chan error, 1)
done <- errors.New(`{"id":"go.micro.client","code":408,"detail":"<nil>","status":"Request Timeout"}`)
+16 -2
View File
@@ -92,11 +92,11 @@ func main() {
if *requireConfiguredFlag {
fmt.Printf("FAIL %s: missing API key (%s)\n", provider, msg)
failed++
results = append(results, conformanceResult{Provider: provider, Status: statusFailed, Error: "missing API key: " + msg})
results = append(results, missingKeyResults(provider, harnesses, statusFailed, "missing API key: "+msg)...)
} else {
fmt.Printf("- %s: skipped (%s)\n", provider, msg)
skipped++
results = append(results, conformanceResult{Provider: provider, Status: statusSkipped, Error: msg})
results = append(results, missingKeyResults(provider, harnesses, statusSkipped, msg)...)
}
continue
}
@@ -166,6 +166,20 @@ type conformanceSummary struct {
Failed int `json:"failed"`
}
func missingKeyResults(provider string, harnesses []string, status, detail string) []conformanceResult {
results := make([]conformanceResult, 0, len(harnesses))
for _, harness := range harnesses {
results = append(results, conformanceResult{
Provider: provider,
Harness: harness,
Phase: harnessPhase(harness),
Status: status,
Error: detail,
})
}
return results
}
func writeSummaryJSON(path string, summary conformanceSummary) error {
b, err := json.MarshalIndent(summary, "", " ")
if err != nil {
@@ -77,6 +77,23 @@ func TestCapabilityMatrixHasRegisteredProviders(t *testing.T) {
}
}
func TestMissingKeyResultsReportsEachHarness(t *testing.T) {
results := missingKeyResults("openai", []string{"agent", "plan-delegate"}, statusSkipped, "set OPENAI_API_KEY")
if len(results) != 2 {
t.Fatalf("missingKeyResults returned %d results, want 2", len(results))
}
wants := []conformanceResult{
{Provider: "openai", Harness: "agent", Phase: harnessPhase("agent"), Status: statusSkipped, Error: "set OPENAI_API_KEY"},
{Provider: "openai", Harness: "plan-delegate", Phase: harnessPhase("plan-delegate"), Status: statusSkipped, Error: "set OPENAI_API_KEY"},
}
for i, want := range wants {
if results[i] != want {
t.Fatalf("result[%d] = %#v, want %#v", i, results[i], want)
}
}
}
func TestWriteCapabilityMarkdown(t *testing.T) {
path := filepath.Join(t.TempDir(), "capabilities.md")
rows := []ai.CapabilityRow{
+59 -10
View File
@@ -269,13 +269,21 @@ func completeNotifyOnObservedSideEffect(ctx context.Context, in flow.State, ntf
in.Data = []byte("Buyer notified.")
return in, nil
}
wait := time.NewTimer(25 * time.Millisecond)
select {
case <-ctx.Done():
if dispatchErr != nil {
return in, dispatchErr
if !wait.Stop() {
<-wait.C
}
return in, ctx.Err()
case <-time.After(25 * time.Millisecond):
if dispatchErr == nil {
return in, ctx.Err()
}
// A timed-out Agent.Chat call can report the caller context as done
// while the remote agent is still finishing its notify tool call.
// Keep watching for the idempotent side effect until the local settle
// window expires so a post-side-effect timeout does not strand the
// durable checkout run as pending.
case <-wait.C:
}
}
if dispatchErr != nil {
@@ -349,14 +357,55 @@ func check(cond bool, format string, args ...any) {
// should not depend on a live model deciding to send another notification.
func a2aReachable(ctx context.Context, base, agent string) error {
probe := "A2A reachability probe only. Reply with the words concierge reachable. Do not call tools or send notifications."
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(ctx, probe)
if err != nil {
return err
deadline, ok := ctx.Deadline()
if !ok {
deadline = time.Now().Add(10 * time.Second)
}
if strings.TrimSpace(reply) == "" {
return fmt.Errorf("empty A2A reply")
var lastErr error
for attempt := 1; ; attempt++ {
if err := ctx.Err(); err != nil {
if lastErr != nil {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
}
return err
}
remaining := time.Until(deadline)
if remaining <= 0 {
if lastErr != nil {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
}
return context.DeadlineExceeded
}
attemptTimeout := 4 * time.Second
if remaining < attemptTimeout {
attemptTimeout = remaining
}
attemptCtx, cancel := context.WithTimeout(ctx, attemptTimeout)
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(attemptCtx, probe)
cancel()
if err == nil && strings.TrimSpace(reply) != "" {
return nil
}
if err == nil {
err = fmt.Errorf("empty A2A reply")
}
lastErr = err
if time.Until(deadline) <= 0 {
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt, lastErr)
}
time.Sleep(minDuration(200*time.Millisecond*time.Duration(attempt), time.Until(deadline)))
}
return nil
}
func minDuration(a, b time.Duration) time.Duration {
if a < b {
return a
}
return b
}
func providerKey(provider string) string {
+64
View File
@@ -3,6 +3,9 @@ package main
import (
"context"
"errors"
"fmt"
"net/http"
"net/http/httptest"
"sync/atomic"
"testing"
"time"
@@ -25,6 +28,31 @@ func TestUniverseHarnessContract(t *testing.T) {
}
}
func TestA2AReachableRetriesTransientTimeout(t *testing.T) {
var calls int64
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if got, want := r.URL.Path, "/agents/concierge"; got != want {
t.Fatalf("path = %q, want %q", got, want)
}
if atomic.AddInt64(&calls, 1) == 1 {
time.Sleep(5 * time.Second)
return
}
w.Header().Set("Content-Type", "application/json")
fmt.Fprint(w, `{"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"task-1","contextId":"ctx-1","status":{"state":"completed"},"artifacts":[{"artifactId":"artifact-1","parts":[{"kind":"text","text":"concierge reachable"}]}]}}`)
}))
defer srv.Close()
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
defer cancel()
if err := a2aReachable(ctx, srv.URL, "concierge"); err != nil {
t.Fatalf("a2aReachable returned error: %v", err)
}
if got := atomic.LoadInt64(&calls); got < 2 {
t.Fatalf("A2A calls = %d, want retry after transient timeout", got)
}
}
func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) {
ntf := new(Notify)
before := atomic.LoadInt64(&ntf.sent)
@@ -69,6 +97,42 @@ func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) {
}
}
func TestNotifyStepWaitsForObservedSideEffectAfterCanceledDispatchContext(t *testing.T) {
ntf := new(Notify)
before := atomic.LoadInt64(&ntf.sent)
ctx, cancel := context.WithCancel(context.Background())
cancel()
go func() {
time.Sleep(30 * time.Millisecond)
var rsp SendResponse
if err := ntf.Send(context.Background(), &SendRequest{
To: "buyer@acme.com",
Message: "Your order is confirmed.",
}, &rsp); err != nil {
t.Errorf("send notification: %v", err)
}
}()
out, err := completeNotifyOnObservedSideEffect(
ctx,
flow.State{Data: []byte(`{"order":"order-1"}`)},
ntf,
before,
time.Second,
errors.New("client observed timeout"),
)
if err != nil {
t.Fatalf("notify completion returned error: %v", err)
}
if got := out.String(); got != "Buyer notified." {
t.Fatalf("result = %q, want Buyer notified.", got)
}
if got := atomic.LoadInt64(&ntf.sent); got != 1 {
t.Fatalf("notifications sent = %d, want 1", got)
}
}
func TestNotifyStepRejectsClaimedCompletionWithoutSideEffect(t *testing.T) {
ntf := new(Notify)
before := atomic.LoadInt64(&ntf.sent)
+203 -8
View File
@@ -7,6 +7,7 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"testing"
"time"
@@ -26,7 +27,7 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
"go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1",
"go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1",
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1",
"./internal/harness/zero-to-hero-ci/run.sh",
"micro zero-to-hero",
"go run ./internal/harness/agent-flow",
@@ -45,7 +46,7 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
"go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestExamplesWayfindingIndexStaysLinked|TestExamplesCommandPointsAtWayfindingIndex|TestZeroToHeroCLIBoundaries|TestZeroToHeroCommandPrintsMaintainedNoSecretPath' -count=1",
"go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1",
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1",
} {
if !strings.Contains(runScript, want) {
t.Fatalf("0→hero CI run script missing lifecycle command %q", want)
@@ -293,6 +294,7 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"micro zero-to-hero",
"internal/website/docs/guides/no-secret-first-agent.md",
"internal/website/docs/guides/your-first-agent.md",
"micro chat",
"internal/website/docs/guides/debugging-agents.md",
"micro inspect agent <name>",
"internal/website/docs/guides/zero-to-hero.md",
@@ -362,6 +364,7 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"https://github.com/micro/go-micro/tree/master/examples/first-agent",
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"micro chat",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
@@ -381,6 +384,7 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"https://github.com/micro/go-micro/tree/master/examples/first-agent",
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"micro chat",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
@@ -396,6 +400,7 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"https://github.com/micro/go-micro/tree/master/examples/support",
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"micro chat",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
@@ -433,6 +438,7 @@ func TestFirstAgentWayfindingCanonicalTrailStaysInSync(t *testing.T) {
"examples/support",
"no-secret-first-agent",
"your-first-agent",
"micro chat",
"debugging-agents",
"micro inspect agent <name>",
"zero-to-hero",
@@ -489,6 +495,7 @@ func TestFirstAgentWayfindingCanonicalTrailStaysInSync(t *testing.T) {
"examples/first-agent",
"examples/support",
"your-first-agent",
"micro chat",
"debugging-agents",
"micro inspect agent <name>",
},
@@ -998,6 +1005,190 @@ func TestNoSecretFirstAgentDebuggingSmoke(t *testing.T) {
}
}
func TestFirstAgentCLIChatInspectFixture(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
absRoot, err := filepath.Abs(root)
if err != nil {
t.Fatalf("resolve repository root: %v", err)
}
workspace := t.TempDir()
home := filepath.Join(workspace, "home")
if err := os.MkdirAll(home, 0o755); err != nil {
t.Fatalf("create fixture home: %v", err)
}
writeFile(t, filepath.Join(workspace, "go.mod"), "module example.com/first-agent-cli-fixture\n\ngo 1.24\n\nrequire go-micro.dev/v6 v6.0.0\n\nreplace go-micro.dev/v6 => "+filepath.ToSlash(absRoot)+"\n")
writeFile(t, filepath.Join(workspace, "main.go"), firstAgentCLIFixtureSource())
runInWorkspace(t, workspace, "go", "mod", "tidy")
micro := buildMicroBinary(t, absRoot)
fixtureBin := filepath.Join(workspace, "first-agent-cli-fixture")
runInWorkspace(t, workspace, "go", "build", "-o", fixtureBin, ".")
fixture := exec.Command(fixtureBin)
fixture.Dir = workspace
fixture.Env = append(os.Environ(),
"HOME="+home,
"MICRO_AI_API_KEY=",
"OPENAI_API_KEY=",
"ANTHROPIC_API_KEY=",
"GEMINI_API_KEY=",
)
var fixtureOut lockedBuffer
fixture.Stdout = &fixtureOut
fixture.Stderr = &fixtureOut
if err := fixture.Start(); err != nil {
t.Fatalf("start first-agent CLI fixture: %v\n%s", err, fixtureOut.String())
}
defer func() {
if fixture.Process != nil {
_ = fixture.Process.Signal(os.Interrupt)
_ = fixture.Process.Kill()
}
}()
waitForCLIOutput(t, &fixtureOut, "first-agent fixture ready", 15*time.Second)
waitForRegisteredAgent(t, micro, home, "assistant", &fixtureOut, 15*time.Second)
chat := runMicroCLIWithHome(t, micro, home, "chat", "--prompt", "Summarize my first-agent next steps", "assistant")
for _, want := range []string{"assistant:", "install the CLI", "run a service", "chat with an agent"} {
if !strings.Contains(chat, want) {
t.Fatalf("micro chat assistant output missing %q:\n%s\nfixture output:\n%s", want, chat, fixtureOut.String())
}
}
stopFixture(t, fixture)
inspect := runMicroCLIWithHome(t, micro, home, "inspect", "agent", "assistant", "--limit", "1")
for _, want := range []string{`Agent "assistant" runs`, "status=done", "last=done"} {
if !strings.Contains(inspect, want) {
t.Fatalf("micro inspect agent assistant output missing %q:\n%s\nfixture output:\n%s", want, inspect, fixtureOut.String())
}
}
}
func stopFixture(t *testing.T, cmd *exec.Cmd) {
t.Helper()
if cmd.Process == nil {
return
}
done := make(chan error, 1)
_ = cmd.Process.Signal(os.Interrupt)
go func() { done <- cmd.Wait() }()
select {
case <-done:
case <-time.After(2 * time.Second):
_ = cmd.Process.Kill()
<-done
}
}
func waitForCLIOutput(t *testing.T, out *lockedBuffer, want string, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
for time.Now().Before(deadline) {
if strings.Contains(out.String(), want) {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("timed out waiting for fixture output %q; got:\n%s", want, out.String())
}
func waitForRegisteredAgent(t *testing.T, micro, home, agent string, fixtureOut *lockedBuffer, timeout time.Duration) {
t.Helper()
deadline := time.Now().Add(timeout)
var lastOut []byte
var lastErr error
for time.Now().Before(deadline) {
cmd := exec.Command(micro, "agent", "list")
cmd.Env = microCLIEnv(home)
lastOut, lastErr = cmd.CombinedOutput()
if lastErr == nil && strings.Contains(string(lastOut), agent) {
return
}
time.Sleep(100 * time.Millisecond)
}
t.Fatalf("timed out waiting for registered agent %q; last micro agent list error: %v\nlast output:\n%s\nfixture output:\n%s", agent, lastErr, lastOut, fixtureOut.String())
}
type lockedBuffer struct {
mu sync.Mutex
b strings.Builder
}
func (b *lockedBuffer) Write(p []byte) (int, error) {
b.mu.Lock()
defer b.mu.Unlock()
return b.b.Write(p)
}
func (b *lockedBuffer) String() string {
b.mu.Lock()
defer b.mu.Unlock()
return b.b.String()
}
func firstAgentCLIFixtureSource() string {
return `package main
import (
"context"
"fmt"
"os"
"os/signal"
"path/filepath"
"strings"
"syscall"
"go-micro.dev/v6/agent"
"go-micro.dev/v6/ai"
"go-micro.dev/v6/service"
"go-micro.dev/v6/store"
)
type NotesService struct{}
type ListNotesRequest struct{}
type ListNotesResponse struct { Notes []string ` + "`json:\"notes\" description:\"Notes the assistant can summarize\"`" + ` }
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
rsp.Notes = []string{"Install the CLI", "Run a service", "Chat with an agent"}
return nil
}
type mockModel struct{ opts ai.Options }
func newMock(opts ...ai.Option) ai.Model { m := &mockModel{}; _ = m.Init(opts...); return m }
func (m *mockModel) Init(opts ...ai.Option) error { for _, o := range opts { o(&m.opts) }; return nil }
func (m *mockModel) Options() ai.Options { return m.opts }
func (m *mockModel) String() string { return "first-agent-cli-fixture" }
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) { return nil, fmt.Errorf("stream unsupported") }
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
for _, tool := range req.Tools { if strings.Contains(tool.Name, "List") && m.opts.ToolHandler != nil { m.opts.ToolHandler(ctx, ai.ToolCall{ID:"list-notes", Name: tool.Name, Input: map[string]any{}}); break } }
return &ai.Response{Answer: "assistant: your first agent should install the CLI, run a service, then chat with an agent."}, nil
}
func main() {
ai.Register("first-agent-cli-fixture", newMock)
home, _ := os.UserHomeDir()
st := store.NewFileStore(store.DirOption(filepath.Join(home, "micro", "store")))
defer st.Close()
svc := service.New(service.Name("notes"), service.Address("127.0.0.1:0"))
if err := svc.Handle(&NotesService{}); err != nil { panic(err) }
go func() { if err := svc.Run(); err != nil { fmt.Println(err); os.Exit(1) } }()
defer svc.Server().Stop()
a := agent.New(agent.Name("assistant"), agent.Address("127.0.0.1:0"), agent.Services("notes"), agent.Provider("first-agent-cli-fixture"), agent.WithStore(st))
go func() { if err := a.Run(); err != nil { fmt.Println(err); os.Exit(1) } }()
defer a.Stop()
fmt.Println("first-agent fixture ready")
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
}
`
}
func seedNoSecretAgentDebuggingState(t *testing.T, st store.Store) {
t.Helper()
scoped := store.Scope(st, "agent", "assistant")
@@ -1038,18 +1229,22 @@ func buildMicroBinary(t *testing.T, root string) string {
func runMicroCLIWithHome(t *testing.T, micro, home string, args ...string) string {
t.Helper()
cmd := exec.Command(micro, args...)
cmd.Env = append(os.Environ(),
cmd.Env = microCLIEnv(home)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}
func microCLIEnv(home string) []string {
return append(os.Environ(),
"HOME="+home,
"MICRO_AI_API_KEY=",
"OPENAI_API_KEY=",
"ANTHROPIC_API_KEY=",
"GEMINI_API_KEY=",
)
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out)
}
return string(out)
}
func TestFirstAgentWayfindingTargetsExist(t *testing.T) {
+2 -2
View File
@@ -25,7 +25,7 @@ run_step "run/chat/inspect: first-agent CLI boundaries" \
run_step "deploy dry-run: configured target plan" \
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
run_step "chat/inspect: no-secret first-agent transcript and docs" \
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestZeroToHeroDeployDryRunCommandSmoke|TestYourFirstAgentTutorialSmoke' -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestFirstAgentCLIChatInspectFixture|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestZeroToHeroDeployDryRunCommandSmoke|TestYourFirstAgentTutorialSmoke' -count=1
# Deterministic no-secret reference scenarios. These use the real Go Micro
# runtime and mock only the LLM provider. The support example is the maintained
@@ -34,6 +34,6 @@ run_step "chat/inspect: no-secret first-agent transcript and docs" \
run_step "first-agent app: runnable provider-free example" \
go test ./examples/first-agent -run TestRunFirstAgent -count=1
run_step "0→hero app: support lifecycle smoke" \
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1
run_step "flow history: deterministic services → agents → workflows harnesses" \
go test ./internal/harness/universe ./internal/harness/plan-delegate -run 'Test.*Harness|TestPlanDelegateEndToEnd|TestPlanDelegateFlowHandoff' -count=1
+16 -7
View File
@@ -104,16 +104,25 @@ If you are new, follow the architecture in the same order the runtime composes i
CLI, `PATH`, version, and no-secret smoke path are healthy.
2. [`micro agent demo`](getting-started.html#first-agent-on-ramp) — print the
provider-free first-agent command and next docs steps from the installed CLI.
3. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)
3. `micro agent quickcheck` (or `micro agent debug`) — print the short recovery
map when scaffold → run → chat → inspect stalls.
4. `micro examples` — list the maintained provider-free runnable examples in
copy/paste order.
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle
harness and runnable examples.
6. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md)
— choose the smallest no-secret first-agent, support reference, and interop
examples from one map.
7. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)
— run one service-backed agent with a mock model.
4. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — see the
8. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — see the
maintained support-agent path work without a provider key.
5. [Your First Agent](guides/your-first-agent.html) — build and chat with a
9. [Your First Agent](guides/your-first-agent.html) — build and chat with a
service-backed agent.
6. [Debugging your agent](guides/debugging-agents.html) — inspect service
registration, tools, memory, providers, and run history.
7. [0→hero Reference](guides/zero-to-hero.html) — walk scaffold → run → chat →
inspect → flow → deploy dry-run as the maintained lifecycle contract.
10. [Debugging your agent](guides/debugging-agents.html) — inspect service
registration, tools, memory, providers, and run history.
11. [0→hero Reference](guides/zero-to-hero.html) — walk scaffold → run → chat →
inspect → flow → deploy dry-run as the maintained lifecycle contract.
## Related
+9 -8
View File
@@ -57,14 +57,15 @@ After this quick start, follow the agent path in order:
1. [Install troubleshooting](guides/install-troubleshooting.html) — verify the CLI install before agent work.
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
5. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support), and next interop examples from one map.
6. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) — run one service-backed agent with a mock model and no provider key.
7. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — run a useful support agent with a mock model before setting up a provider key.
8. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
9. [Debugging your agent](guides/debugging-agents.html) — use `micro inspect agent <name>` to inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
10. [0→hero reference path](guides/zero-to-hero.html) — prove the full scaffold → run → chat → inspect → deploy dry-run lifecycle with commands exercised by `make harness`.
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
6. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support), and next interop examples from one map.
7. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) — run one service-backed agent with a mock model and no provider key.
8. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — run a useful support agent with a mock model before setting up a provider key.
9. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
10. [Debugging your agent](guides/debugging-agents.html) — use `micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`, then `micro chat` and `micro inspect agent <name>` to recover service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
11. [0→hero reference path](guides/zero-to-hero.html) — prove the full scaffold → run → chat → inspect → deploy dry-run lifecycle with commands exercised by `make harness`.
## Write a Service
@@ -24,10 +24,11 @@ Use the recovery command that matches where you are in the first-agent journey:
| Checkpoint | When to use it | Command |
| --- | --- | --- |
| Install troubleshooting | `micro` is not installed, not on `PATH`, or the shell cannot run it. | [Install troubleshooting](install-troubleshooting.html) |
| Quick recovery map | The first-agent loop stalled and you want the short scaffold → run → chat → inspect checklist before reading this full guide. | `micro agent quickcheck` (alias: `micro agent debug`) |
| Preflight before `micro run` | You have not started the local runtime yet and want to verify Go, CLI, provider-key, and gateway-port prerequisites. | `micro agent preflight` |
| Doctor after `micro run` | `micro run` is active, but chat, the `/agent` gateway, agent registration, provider settings, or inspect/run history is not behaving. | `micro agent doctor` |
`micro agent preflight` is read-only and runs before the first local run; failed
`micro agent quickcheck` is the quickest breadcrumb when you are unsure where the first-agent path failed: it prints the preflight, run, doctor, inspect, and no-secret fallback commands in one place. `micro agent preflight` is read-only and runs before the first local run; failed
checks include `Fix:` and `Next:` lines for Go, CLI installation, provider-key
setup, and the local gateway port. Once `micro run` is already up, switch to
`micro agent doctor` so the recovery output follows the live gateway, chat
+2 -2
View File
@@ -25,7 +25,7 @@ cloud credentials?"
| Inspect | `micro inspect agent <name>`, `micro agent history <name>`, `micro inspect flow <flow>`, and `micro flow runs <flow>` remain discoverable for run history; the no-secret debugging smoke seeds durable agent history and runs the documented inspect/history commands without provider keys. | `go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1` |
| Deploy | `micro deploy --dry-run prod` resolves the documented deploy target without touching remote infrastructure. | `go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1` |
| Smallest first agent | `examples/first-agent` runs one service-backed agent with a deterministic mock model and no provider key. | `go test ./examples/first-agent -run TestRunFirstAgent -count=1` |
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1` |
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1` |
| Runtime harnesses | Real services, agents, durable flows, store-backed history, delegation, and A2A run with only the model mocked. | `./internal/harness/zero-to-hero-ci/run.sh` and `make provider-conformance-mock` |
## Find the one-command entrypoint
@@ -96,7 +96,7 @@ go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunComman
go test ./examples/first-agent -run TestRunFirstAgent -count=1
# Maintained 0→hero support-desk reference app.
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1
# Durable services → agents → workflows reference scenarios.
./internal/harness/zero-to-hero-ci/run.sh
+3 -3
View File
@@ -16,7 +16,7 @@ It's built on a pluggable architecture of Go interfaces: service discovery, clie
## Learn More
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) for the fastest provider-free run, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build and chat with a service-backed agent, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) for the fastest provider-free run, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build a service-backed agent and talk to it with `micro chat`, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
Otherwise continue to read the docs for more information about the framework.
@@ -30,7 +30,7 @@ Otherwise continue to read the docs for more information about the framework.
- [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) - Run one service-backed agent with a deterministic mock model
- [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) - Run the maintained no-secret services → agents → workflows example
- [No-secret first-agent transcript](guides/no-secret-first-agent.html) - Run the first useful agent path without a provider key
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent end to end
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent and talk to it with `micro chat`
- [MCP & AI Agents](mcp.html) - Turn services into AI-callable tools with the Model Context Protocol
- [CLI & Gateway Guide](guides/cli-gateway.html) - Development vs Production modes
- [`micro loop` quickstart](guides/micro-loop.html) - Scaffold an autonomous CI-gated improvement loop
@@ -55,7 +55,7 @@ Otherwise continue to read the docs for more information about the framework.
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → `micro inspect agent <name>` → deploy dry-run with CI-backed commands
- [No-secret first-agent transcript](guides/no-secret-first-agent.html) - Run the first useful agent path without a provider key
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent end to end
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent and talk to it with `micro chat`
- [Building AI-Native Services](guides/ai-native-services.html) - End-to-end tutorial for MCP-enabled services
- [MCP Security Guide](guides/mcp-security.html) - Auth, scopes, rate limiting, and audit logging
- [Tool Description Best Practices](guides/tool-descriptions.html) - Writing docs that make agents effective