Compare commits

..

41 Commits

Author SHA1 Message Date
Codex 65b86a9274 docs: refresh planner priorities for 4168
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-06 18:51:03 +00:00
Asim Aslam dbce523437 agent: cover durable checkpoint resume smoke (#4167)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 19:29:11 +01:00
Asim Aslam 98125cd770 Stabilize AtlasCloud follow-up tool fallback (#4165)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 18:37:42 +01:00
Asim Aslam 3454a08079 docs: refresh planner priorities for 4160 (#4161)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 18:10:32 +01:00
Asim Aslam decfa7c63e Fix AtlasCloud tool schema normalization (#4159)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 16:58:24 +01:00
Asim Aslam b6980e27a9 docs: refresh planner priorities for 4154 (#4155)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 16:17:30 +01:00
Asim Aslam d92d63943c Add no-secret agent debugging smoke (#4153)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 15:08:34 +01:00
Asim Aslam ec1d46526c docs: refresh planner priorities for 4147 (#4149)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 14:28:46 +01:00
Asim Aslam 40308cf779 Preserve delegated notification plan completion (#4146)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 13:03:39 +01:00
Asim Aslam e198810390 docs: refresh planner priorities for 4141 (#4143)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 12:44:39 +01:00
Asim Aslam 3ec50d1a7d Add first-agent tutorial smoke harness (#4140)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 11:06:12 +01:00
Asim Aslam 327f99cd19 docs: refresh coherence changelog (#4135)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 09:40:01 +01:00
Asim Aslam 498aac59f4 Stabilize duplicate plan-delegate notify replays (#4133)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 09:12:58 +01:00
Asim Aslam 6833b3c73c docs: refresh planner priorities for 4127 (#4131)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 08:46:21 +01:00
Asim Aslam ec27ce2e25 Fix first-agent quickstart numbering (#4125)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 07:18:03 +01:00
Asim Aslam 45cf24162b Add first-agent examples CLI wayfinding (#4124)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 07:17:16 +01:00
Asim Aslam 2778472096 docs: refresh planner priorities for 4121 (#4122)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 06:39:18 +01:00
Asim Aslam 9dfb35d85a Guard provider conformance workflow scheduling (#4120)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 04:59:35 +01:00
Asim Aslam ac230b57ee docs: refresh planner priorities for 4114 (#4116)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 04:18:25 +01:00
Asim Aslam 96eea598fc docs: align first-agent inspect command (#4113)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 03:30:28 +01:00
Asim Aslam 97391e9a92 docs: refresh planner priorities for 4109 (#4111)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 02:48:05 +01:00
Asim Aslam 6f2fefc1e1 Make plan-delegate notify replay idempotent (#4108)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 02:22:20 +01:00
Asim Aslam 224446b948 docs: refresh planner priorities for 4103 (#4105)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 01:44:17 +01:00
Asim Aslam eb16370f03 Add zero-to-hero CLI entrypoint (#4102)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 01:02:09 +01:00
Asim Aslam 3262034698 docs: refresh planner priorities for 4096 (#4098)
Co-authored-by: Codex <codex@openai.com>
2026-07-06 00:31:40 +01:00
Asim Aslam 2963ac3fa6 docs: align architecture with agent lifecycle (#4095)
goreleaser / goreleaser (push) Waiting to run
Co-authored-by: Codex <codex@openai.com>
2026-07-05 23:59:15 +01:00
Asim Aslam eaff193569 docs: refresh planner priorities for 4091 (#4093)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 23:35:22 +01:00
Asim Aslam 36fd5b7bcd Promote first-agent doctor recovery (#4090)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 22:59:00 +01:00
Asim Aslam 2e89b01295 docs: refresh planner priorities for 4085 (#4087)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 22:34:11 +01:00
Asim Aslam e95d565502 docs: add install troubleshooting on-ramp (#4084)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 21:59:12 +01:00
Asim Aslam a826e01dd4 docs: refresh planner queue for 4081 (#4082)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 21:33:35 +01:00
Asim Aslam f8619d89b6 docs: align website quickstart on-ramp (#4080)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 21:06:30 +01:00
Asim Aslam 8ddb71143a docs: refresh planner priorities for 4076 (#4078)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 20:35:10 +01:00
Asim Aslam 3699c88e10 Make config close idempotent (#4075)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 20:08:06 +01:00
Asim Aslam a2145cc3a9 docs: refresh planner priorities for 4070 (#4072)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 19:39:21 +01:00
Asim Aslam d0dce12797 test docs wayfinding link targets (#4069)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 19:01:31 +01:00
Asim Aslam 2cfa776a66 docs: refresh planner priorities for 4063 (#4065)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 18:43:42 +01:00
Asim Aslam 24a64aadb0 test examples lifecycle map (#4062)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 18:01:38 +01:00
Asim Aslam 5d3d570c4f docs: refresh planner priorities for 4058 (#4060)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 17:36:44 +01:00
Asim Aslam 304d14331c docs: lead getting started with no-secret path (#4057)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 17:09:01 +01:00
Asim Aslam 5429ff0f08 docs: refresh planner priorities for 4053 (#4055)
Co-authored-by: Codex <codex@openai.com>
2026-07-05 16:39:33 +01:00
40 changed files with 1506 additions and 209 deletions
+2 -1
View File
@@ -21,7 +21,8 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Lead Getting Started with the no-secret first-run path** ([#4054](https://github.com/micro/go-micro/issues/4054)) — #4039 added `micro agent demo`, #4044 documented it in the README/website on-ramp, #4049 moved the installed CLI docs to lead with it, and #4052 surfaced it from scaffold next steps. The remaining adoption seam is the website Getting Started page: it still introduces provider-key setup and prompt generation before the no-secret scaffold → run → call path, so a new developer can infer an LLM key is mandatory before they have proved the runtime works. Reorder Getting Started to lead with install → `micro new``micro run` → curl, then `micro agent demo` and the no-secret first-agent sequence, and add a focused docs/harness assertion so the README, CLI docs, and website cannot drift apart again.
1. **Make AtlasCloud delegated notification side effects exact-once** ([#4163](https://github.com/micro/go-micro/issues/4163)) — the prior AtlasCloud schema/fallback fixes closed #4157, but the latest live provider-conformance signal shows a remaining Now-phase reliability seam: the `plan-delegate` harness can produce duplicate delegated `notify` side effects before the exact-once guard fails. Fixing this first keeps the same services → agents → workflows story portable across providers without changing public APIs.
2. **Add `micro loop` quickstart wayfinding to README and website docs** ([#4169](https://github.com/micro/go-micro/issues/4169)) — the blog now says the autonomous loop is shipped and reusable, but the lived developer on-ramp still depends on finding the launch post. Surface `micro loop init` / `micro loop verify`, token setup, branch protection, and CI-as-gate expectations in durable docs so the harness-building-itself story is adoptable, not just announced.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
harness-live:
name: Provider harnesses (live LLM conformance)
runs-on: ubuntu-latest
# Only on the daily schedule or a manual run — never automatically on
# Only on the hourly schedule or a manual run — never automatically on
# every push/PR, so changes don't quietly burn API credits. Trigger it
# by hand (Actions → Harness → Run workflow) when changing the agent,
# flow, or AI internals and you want a real-model check.
+34
View File
@@ -16,6 +16,40 @@ next version when it ships.
## [Unreleased]
### Added
- **First-agent examples CLI wayfinding** — `micro examples` now prints the maintained provider-free first-agent examples in copy/paste order. (`cmd/micro/`)
- **0→hero CLI entrypoint** — `micro zero-to-hero` now points developers at the maintained no-secret services → agents → workflows harness and runnable examples. (`cmd/micro/`)
### Fixed
- **Plan/delegate notify replays** — duplicate and replayed plan-delegate notifications are now idempotent, so resumed runs do not duplicate completed notifications. (`agent/`, `internal/harness/`)
- **Provider conformance scheduling** — provider conformance workflow dispatches now guard their scheduling path more reliably. (`.github/workflows/`)
### Documentation
- **First-agent quickstart numbering** — the first-agent on-ramp numbering is consistent across the README and website docs. (`README.md`, `internal/website/docs/`)
- **First-agent inspect command** — docs now use the maintained `micro inspect agent <name>` form. (`README.md`, `internal/website/docs/`)
---
## [6.3.16] - July 2026
### Added
- **No-secret agent demo CLI** — the CLI now surfaces `micro agent demo`, making the provider-free first-agent path discoverable from the installed binary. (`cmd/micro/`)
- **First-agent recovery doctor** — first-agent recovery checks now help diagnose install, scaffold, and provider setup issues before the live agent run. (`cmd/micro/`, `internal/website/docs/guides/`)
### Changed
- **Architecture lifecycle docs** — the architecture guide now leads with the services → agents → workflows lifecycle and the first-agent on-ramp. (`internal/website/docs/architecture.md`)
- **First-agent on-ramp** — README and website docs now lead new users through install troubleshooting, no-secret demos, the smallest first-agent example, debugging, and the 0→hero reference path in the same order. (`README.md`, `internal/website/docs/`)
### Fixed
- **Config close idempotency** — config close paths now tolerate repeated closes safely. (`config/`)
- **OpenTelemetry child span events** — agent traces now preserve child span events more reliably. (`agent/`)
### Documentation
- **Security reporting** — security docs now route vulnerability reports through GitHub Security Advisories. (`SECURITY.md`, `internal/website/docs/`)
- **Install troubleshooting** — the first-agent on-ramp now includes clearer install and PATH recovery guidance. (`internal/website/docs/guides/install-troubleshooting.md`)
---
## [6.3.15] - July 2026
### Added
+12 -7
View File
@@ -50,6 +50,8 @@ curl -fsSL https://go-micro.dev/install.sh | sh
go install go-micro.dev/v6/cmd/micro@latest
```
If install or `PATH` checks fail, use the [install troubleshooting guide](internal/website/docs/guides/install-troubleshooting.md) before scaffolding your first service.
### Fastest start — no API key
Scaffold a service, run it, call it:
@@ -87,16 +89,19 @@ make harness
After install and the first `micro new`/`micro run` smoke check, take the
walkable agent path in this order:
1. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
2. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
3. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
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. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
6. [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.
4. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
7. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
service-backed agent and talk to it with `micro chat`.
5. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
`micro agent inspect`, run history, memory, and provider checks when the first
8. [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.
6. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
9. [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.
+23 -2
View File
@@ -400,7 +400,7 @@ func preserveCompletedPlanSteps(stored string, input map[string]any) map[string]
continue
}
task, _ := step["task"].(string)
if completed[normalizePlanTask(task)] && isUnfinishedPlanStatus(step["status"]) {
if completed[planTaskCompletionKey(task)] && isUnfinishedPlanStatus(step["status"]) {
step["status"] = "done"
}
}
@@ -423,7 +423,7 @@ func completedPlanTasks(plan map[string]any) map[string]bool {
continue
}
task, _ := step["task"].(string)
if task = normalizePlanTask(task); task != "" {
if task = planTaskCompletionKey(task); task != "" {
completed[task] = true
}
}
@@ -434,6 +434,27 @@ func normalizePlanTask(task string) string {
return strings.Join(strings.Fields(strings.ToLower(task)), " ")
}
func planTaskCompletionKey(task string) string {
normalized := normalizePlanTask(task)
if normalized == "" {
return ""
}
if isLaunchReadinessDelegationPlanTask(normalized) {
return "launch-readiness-notification"
}
return normalized
}
func isLaunchReadinessDelegationPlanTask(task string) bool {
task = normalizePlanTask(task)
if !strings.Contains(task, "notify") && !strings.Contains(task, "notification") {
return false
}
hasLaunchReadiness := strings.Contains(task, "launch") || strings.Contains(task, "readiness") || strings.Contains(task, "ready")
hasOwnerComms := strings.Contains(task, "owner") && strings.Contains(task, "comms")
return hasLaunchReadiness || hasOwnerComms
}
func isUnfinishedPlanStatus(status any) bool {
s, _ := status.(string)
return s == "" || s == "pending" || s == "in_progress"
+21
View File
@@ -79,6 +79,27 @@ func TestHandlePlanPreservesCompletedSteps(t *testing.T) {
}
}
func TestHandlePlanPreservesCompletedLaunchReadinessNotification(t *testing.T) {
mem := store.NewMemoryStore()
a := New(Name("planner"), WithStore(mem)).(*agentImpl)
a.handlePlan(ai.ToolCall{Name: toolPlan, Input: map[string]any{
"steps": []any{
map[string]any{"task": "notify owner via comms", "status": "done"},
},
}})
a.handlePlan(ai.ToolCall{Name: toolPlan, Input: map[string]any{
"steps": []any{
map[string]any{"task": "Delegate launch readiness notification for owner@acme.com to comms agent", "status": "in_progress"},
},
}})
if unfinished := a.unfinishedPlanSteps(); len(unfinished) != 0 {
t.Fatalf("unfinished plan steps = %v, want launch readiness notification preserved as done", unfinished)
}
}
func TestPlanShowsInPrompt(t *testing.T) {
mem := store.NewMemoryStore()
a := New(Name("planner"), Prompt("base prompt"), WithStore(mem)).(*agentImpl)
+5 -1
View File
@@ -50,9 +50,13 @@ func (a *agentImpl) saveRun(ctx context.Context, run flow.Run) error {
return fmt.Errorf("agent %s checkpoint save: %w", a.opts.Name, err)
}
if info, ok := ai.RunInfoFrom(ctx); ok {
stage := run.State.Stage
if stage == "" && len(run.Steps) > 0 {
stage = run.Steps[0].Name
}
a.recordTimelineEvent(ctx, RunEvent{
Time: time.Now(), RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent,
Kind: "checkpoint", Name: run.State.Stage, Status: run.Status,
Kind: "checkpoint", Name: stage, Status: run.Status,
})
}
return nil
+44 -2
View File
@@ -287,7 +287,8 @@ func TestCheckpointContinuesRunThroughSeveralSingleStepTurns(t *testing.T) {
func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
ctx := context.Background()
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "restart-resume-agent")
st := store.NewMemoryStore()
cp := flow.StoreCheckpoint(st, "restart-resume-agent")
toolRuns := 0
modelCalls := 0
failFirst := true
@@ -308,7 +309,7 @@ func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
defer func() { fakeGen = nil }()
newAgent := func() *agentImpl {
return newTestAgent(Name("restart-resume-agent"), WithCheckpoint(cp),
return newTestAgent(Name("restart-resume-agent"), WithStore(st), WithCheckpoint(cp),
WithTool("external.provision", "provision service once", nil, func(context.Context, map[string]any) (string, error) {
toolRuns++
return "provisioned", nil
@@ -330,6 +331,19 @@ func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
if len(runs) != 1 {
t.Fatalf("Pending before restart returned %d runs, want 1", len(runs))
}
summaries, err := ListRunSummaries(st, "restart-resume-agent")
if err != nil {
t.Fatalf("ListRunSummaries before restart: %v", err)
}
if len(summaries) != 1 {
t.Fatalf("run summaries before restart = %d, want 1", len(summaries))
}
if summaries[0].RunID != runs[0].ID || summaries[0].Status != "error" || summaries[0].Checkpoint != "failed" || summaries[0].Stage != agentAskStep {
t.Fatalf("summary before restart = %#v, want failed ask checkpoint for %s", summaries[0], runs[0].ID)
}
if summaries[0].Events < 4 || summaries[0].LastError == "" {
t.Fatalf("summary before restart lacks debug history/error: %#v", summaries[0])
}
restarted := newAgent()
resp, err := Resume(ctx, restarted, runs[0].ID)
@@ -352,6 +366,34 @@ func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
if loaded.Status != "done" || loaded.ParentID != runs[0].ParentID {
t.Fatalf("loaded run status/parent = %s/%s, want done/%s", loaded.Status, loaded.ParentID, runs[0].ParentID)
}
summaries, err = ListRunSummaries(st, "restart-resume-agent")
if err != nil {
t.Fatalf("ListRunSummaries after restart: %v", err)
}
if len(summaries) != 1 {
t.Fatalf("run summaries after restart = %d, want 1", len(summaries))
}
if summaries[0].RunID != runs[0].ID || summaries[0].Status != "done" || summaries[0].Checkpoint != "done" || summaries[0].Stage != agentAskStep {
t.Fatalf("summary after restart = %#v, want done ask checkpoint for %s", summaries[0], runs[0].ID)
}
if summaries[0].Events < 7 {
t.Fatalf("summary after restart recorded %d events, want durable failure/resume/done history", summaries[0].Events)
}
events, err := LoadRunEvents(st, "restart-resume-agent", runs[0].ID)
if err != nil {
t.Fatalf("LoadRunEvents after restart: %v", err)
}
seen := map[string]bool{"run": false, "tool": false, "checkpoint": false, "error": false, "resume": false, "done": false}
for _, e := range events {
if _, ok := seen[e.Kind]; ok {
seen[e.Kind] = true
}
}
for kind, ok := range seen {
if !ok {
t.Fatalf("events after restart missing %s: %#v", kind, events)
}
}
}
func TestResumeFailedCheckpointDoesNotDuplicateCompactedMemory(t *testing.T) {
+1 -1
View File
@@ -624,7 +624,7 @@ func runStatus(events []RunEvent) string {
if e.Error != "" || e.Kind == "error" {
status = runErrorStatus(e.ErrorKind)
}
if e.Kind == "done" && status == "running" {
if e.Kind == "done" {
status = "done"
}
}
+65 -15
View File
@@ -93,20 +93,7 @@ func (p *Provider) Options() ai.Options { return p.opts }
func (p *Provider) String() string { return "atlascloud" }
func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
var tools []map[string]any
for _, t := range req.Tools {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": t.Properties,
},
},
})
}
tools := atlascloudTools(req.Tools)
messages := []map[string]any{
{"role": "system", "content": req.SystemPrompt},
@@ -191,7 +178,17 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen
resp.ToolCalls = allToolCalls
}
if followUpResp.Reply != "" {
resp.Answer = followUpResp.Reply
if strings.Contains(followUpResp.Reply, "<tool_call") || strings.Contains(followUpResp.Reply, "function=") {
// Preserve follow-up assistant content as Reply, not Answer, when
// it may contain a text-encoded tool call. The agent harness
// inspects Reply for text fallback calls after Generate returns,
// which covers AtlasCloud/minimax turns that emit a second
// required call (for example guarded delegate) as markup instead
// of native tool_calls.
resp.Reply = followUpResp.Reply
} else {
resp.Answer = followUpResp.Reply
}
} else if len(toolResults) > 0 {
resp.Answer = strings.Join(toolResults, "\n")
}
@@ -379,6 +376,59 @@ func (p *Provider) callAPI(ctx context.Context, phase string, req map[string]any
return response, rawMessage, nil
}
func atlascloudTools(input []ai.Tool) []map[string]any {
tools := make([]map[string]any, 0, len(input))
for _, t := range input {
tools = append(tools, map[string]any{
"type": "function",
"function": map[string]any{
"name": t.Name,
"description": t.Description,
"parameters": map[string]any{
"type": "object",
"properties": normalizeAtlasCloudSchema(t.Properties),
},
},
})
}
return tools
}
func normalizeAtlasCloudSchema(schema map[string]any) map[string]any {
if schema == nil {
return nil
}
out := make(map[string]any, len(schema))
for k, v := range schema {
out[k] = normalizeAtlasCloudSchemaValue(v)
}
return out
}
func normalizeAtlasCloudSchemaValue(v any) any {
switch val := v.(type) {
case map[string]any:
out := make(map[string]any, len(val)+1)
for k, nested := range val {
out[k] = normalizeAtlasCloudSchemaValue(nested)
}
if typ, _ := out["type"].(string); typ == "array" {
if _, ok := out["items"]; !ok {
out["items"] = map[string]any{}
}
}
return out
case []any:
out := make([]any, len(val))
for i, nested := range val {
out[i] = normalizeAtlasCloudSchemaValue(nested)
}
return out
default:
return v
}
}
func normalizeAtlasCloudToolCalls(toolCalls []atlasToolCall) []map[string]any {
out := make([]map[string]any, 0, len(toolCalls))
for _, tc := range toolCalls {
+100
View File
@@ -289,6 +289,58 @@ func TestProvider_GenerateMinimaxToolRequests(t *testing.T) {
}
}
func TestProvider_GenerateNormalizesBuiltInToolSchemas(t *testing.T) {
var body map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"ok"}}]}`))
}))
defer ts.Close()
planProperties := map[string]any{
"steps": map[string]any{
"type": "array",
"description": "ordered plan steps",
},
}
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
_, err := p.Generate(context.Background(), &ai.Request{
Prompt: "plan and delegate",
Tools: []ai.Tool{
{Name: "task_TaskService_Add", Description: "add task", Properties: map[string]any{"title": map[string]any{"type": "string"}}},
{Name: "plan", Description: "record a plan", Properties: planProperties},
{Name: "request_input", Description: "request input", Properties: map[string]any{"prompt": map[string]any{"type": "string"}}},
{Name: "delegate", Description: "delegate work", Properties: map[string]any{"task": map[string]any{"type": "string"}, "to": map[string]any{"type": "string"}}},
},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
tools := body["tools"].([]any)
if len(tools) != 4 {
t.Fatalf("tools = %d, want custom tool plus built-ins", len(tools))
}
planTool := tools[1].(map[string]any)
fn := planTool["function"].(map[string]any)
params := fn["parameters"].(map[string]any)
props := params["properties"].(map[string]any)
steps := props["steps"].(map[string]any)
if _, ok := steps["items"].(map[string]any); !ok {
t.Fatalf("plan steps schema = %#v, want array items for AtlasCloud/minimax", steps)
}
if _, mutated := planProperties["steps"].(map[string]any)["items"]; mutated {
t.Fatalf("Generate mutated caller tool schema: %#v", planProperties)
}
}
func TestProvider_GenerateExecutesFollowUpToolCall(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
@@ -354,6 +406,54 @@ func TestProvider_GenerateExecutesFollowUpToolCall(t *testing.T) {
}
}
func TestProvider_GeneratePreservesFollowUpTextToolCallInReply(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")
switch len(bodies) {
case 1:
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"","tool_calls":[{"id":"call-1","function":{"name":"conformance_echo","arguments":"{\"value\":\"agent-conformance\"}"}}]}}]}`))
case 2:
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"delegate\">{\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}</tool_call>"}}]}`))
default:
t.Fatalf("unexpected API call %d", len(bodies))
}
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithToolHandler(func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
if call.Name != "conformance_echo" {
t.Fatalf("unexpected structured tool call %+v", call)
}
return ai.ToolResult{ID: call.ID, Content: `{"marker":"agent-conformance-ok"}`}
}),
)
resp, err := p.Generate(context.Background(), &ai.Request{
Prompt: "run conformance",
Tools: []ai.Tool{
{Name: "conformance_echo", Description: "echo conformance marker", Properties: map[string]any{"value": map[string]any{"type": "string"}}},
{Name: "delegate", Description: "delegate work", Properties: map[string]any{"task": map[string]any{"type": "string"}, "to": map[string]any{"type": "string"}}},
},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
if !strings.Contains(resp.Reply, `<tool_call name="delegate">`) {
t.Fatalf("Reply = %q, want tagged delegate follow-up for agent text fallback", resp.Reply)
}
if resp.Answer != "" {
t.Fatalf("Answer = %q, want follow-up text preserved only as Reply", resp.Answer)
}
}
func TestProvider_GenerateToolCallHTTPErrorIncludesRequestContext(t *testing.T) {
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, `{"code":400,"msg":"bad request"}`, http.StatusBadRequest)
+20
View File
@@ -52,6 +52,26 @@ This starts:
Open http://localhost:8080 to see your services and call them from the browser.
Call the generated service from another terminal:
```
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
-H 'Content-Type: application/json' -d '{"name":"World"}'
```
## First agent on-ramp
Once the scaffold → run → call path works, ask the installed CLI for the
provider-free agent path:
```
micro agent demo
micro examples
```
Those commands point at the smallest mock-model first-agent example, the no-secret
transcript, and the support app before you add provider-backed chat.
### Output
```
+3 -2
View File
@@ -33,9 +33,10 @@ After it passes:
- Walk the full 0→hero lifecycle: https://go-micro.dev/docs/guides/zero-to-hero.html
Use live-provider chat when you are ready for real model behavior:
micro agent preflight
micro agent preflight # before micro run: prerequisites
micro run
micro chat
micro agent doctor # after micro run: chat/gateway/inspect recovery
micro inspect agent <name>`
func init() {
@@ -80,7 +81,7 @@ for live-provider chat and inspect/debugging.`,
},
{
Name: "doctor",
Usage: "Diagnose chat and inspect recovery after micro run",
Usage: "Diagnose chat, gateway, registration, provider, and inspect recovery after micro run",
Flags: []cli.Flag{
&cli.StringFlag{Name: "gateway", Value: "http://localhost:8080", Usage: "Gateway URL started by micro run"},
},
+75 -4
View File
@@ -24,6 +24,55 @@ import (
_ "go-micro.dev/v6/cmd/micro/cli/remote"
)
const zeroToHeroHelp = `0→hero no-secret lifecycle demo
Run this from a go-micro repository checkout when you want one command that
proves the maintained services → agents → workflows path without provider keys:
./internal/harness/zero-to-hero-ci/run.sh
That script runs the same deterministic path CI uses:
- CLI discovery for scaffold, run, chat, inspect, flow runs, and deploy dry-run
- the smallest first-agent example
- the support-desk reference app with services, an agent, a flow, and an approval gate
- plan/delegate and universe harnesses with only the model mocked
If you only want the runnable examples first:
go run ./examples/first-agent
go run ./examples/support
Full local contract:
make harness
Guide: https://go-micro.dev/docs/guides/zero-to-hero.html`
const examplesWayfinding = `First-agent examples (no provider key required)
Run these from a go-micro repository checkout in this order:
1. Smallest service-backed agent
go run ./examples/first-agent
Proves an agent can call a service tool with the deterministic mock model.
2. No-secret support-agent transcript
go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1
Exercises service tools, mock-model chat, and inspectable run history.
3. Full services → agents → workflows reference app
go run ./examples/support
Shows the support desk service, agent, workflow, and approval gate together.
Then continue the same path with the installed CLI:
micro agent demo
micro docs
micro zero-to-hero
Guides:
https://go-micro.dev/docs/guides/no-secret-first-agent.html
https://go-micro.dev/docs/guides/your-first-agent.html
https://go-micro.dev/docs/guides/debugging-agents.html
https://go-micro.dev/docs/guides/zero-to-hero.html`
const docsWayfinding = `First-agent and 0→hero docs:
1. Start with the no-secret CLI demo
@@ -40,17 +89,17 @@ const docsWayfinding = `First-agent and 0→hero docs:
3. Your First Agent
https://go-micro.dev/docs/guides/your-first-agent.html
Build a service-backed agent, then use:
micro agent preflight
micro agent preflight # before micro run: prerequisites
micro run
micro chat
micro agent doctor
micro agent doctor # after micro run: chat/gateway/inspect recovery
4. Debugging your agent
https://go-micro.dev/docs/guides/debugging-agents.html
Inspect agent runs and memory with:
micro agent doctor
micro inspect agent
micro runs <agent>
micro inspect agent <name>
micro agent history <name>
5. 0→hero Reference
https://go-micro.dev/docs/guides/zero-to-hero.html
@@ -128,6 +177,28 @@ func init() {
return nil
},
},
{
Name: "examples",
Usage: "Show provider-free first-agent example paths",
Description: `Print the maintained no-secret examples for the services → agents →
workflows on-ramp: first-agent, transcript, support app, and matching guides.`,
Action: func(ctx *cli.Context) error {
fmt.Fprintln(ctx.App.Writer, examplesWayfinding)
return nil
},
},
{
Name: "zero-to-hero",
Usage: "Show the no-secret 0→hero lifecycle demo command",
Description: `Print the maintained provider-free services → agents → workflows
lifecycle command and the smaller runnable examples it covers.`,
Aliases: []string{"hero"},
Action: func(ctx *cli.Context) error {
fmt.Fprintln(ctx.App.Writer, zeroToHeroHelp)
return nil
},
},
{
Name: "docs",
Usage: "Show the first-agent and 0→hero documentation path",
+2 -2
View File
@@ -67,7 +67,7 @@ func TestPrintNextStepsSurfacesFirstAgentPath(t *testing.T) {
"micro agent preflight",
"go run .",
"micro chat",
"micro inspect agent",
"micro inspect agent <name>",
"micro agent demo",
"micro docs",
"your-first-agent.html",
@@ -84,7 +84,7 @@ func TestPrintNextStepsNoMCPSkipsMCPHints(t *testing.T) {
var out bytes.Buffer
printNextSteps(&out, "worker", true)
for _, want := range []string{"micro agent preflight", "micro chat", "micro inspect agent", "micro agent demo", "micro docs"} {
for _, want := range []string{"micro agent preflight", "micro chat", "micro inspect agent <name>", "micro agent demo", "micro docs"} {
if !strings.Contains(out.String(), want) {
t.Fatalf("--no-mcp next steps missing %q:\n%s", want, out.String())
}
+1 -1
View File
@@ -291,7 +291,7 @@ func printNextSteps(w io.Writer, dir string, noMCP bool) {
fmt.Fprintln(w, " micro agent preflight")
fmt.Fprintln(w, " go run .")
fmt.Fprintln(w, " micro chat")
fmt.Fprintln(w, " micro inspect agent")
fmt.Fprintln(w, " micro inspect agent <name>")
fmt.Fprintln(w)
fmt.Fprintln(w, " First-agent path:")
fmt.Fprintln(w, " micro agent demo")
+43 -5
View File
@@ -22,7 +22,7 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
}
}
for _, want := range []string{"new", "run", "chat", "inspect", "agent", "docs"} {
for _, want := range []string{"new", "run", "chat", "inspect", "agent", "docs", "examples"} {
if !commands[want] {
t.Fatalf("first-agent walkthrough missing %q command", want)
}
@@ -64,21 +64,58 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
"your-first-agent.html",
"debugging-agents.html",
"zero-to-hero.html",
"micro agent preflight",
"micro agent preflight # before micro run: prerequisites",
"micro run",
"micro chat",
"micro agent doctor",
"micro inspect agent",
"micro agent doctor # after micro run: chat/gateway/inspect recovery",
"micro inspect agent <name>",
"micro agent history <name>",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("micro docs output missing %q:\n%s", want, out.String())
}
}
if strings.Contains(out.String(), "micro runs") {
t.Fatalf("micro docs output should use the first-agent inspect command, not the legacy runs shortcut:\n%s", out.String())
}
examples := commandByName(t, "examples")
if !strings.Contains(examples.Usage, "first-agent") {
t.Fatalf("micro examples should advertise the first-agent examples path; usage was %q", examples.Usage)
}
out.Reset()
if err := examples.Action(cli.NewContext(app, nil, nil)); err != nil {
t.Fatalf("micro examples failed: %v", err)
}
for _, want := range []string{
"First-agent examples",
"go run ./examples/first-agent",
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
"go run ./examples/support",
"micro agent demo",
"micro docs",
"micro zero-to-hero",
"no-secret-first-agent.html",
"your-first-agent.html",
"debugging-agents.html",
"zero-to-hero.html",
} {
if !strings.Contains(out.String(), want) {
t.Fatalf("micro examples output missing %q:\n%s", want, out.String())
}
}
agent := commandByName(t, "agent")
if !strings.Contains(agent.Usage, "micro agent demo") {
t.Fatalf("micro agent help should advertise the no-secret demo; usage was %q", agent.Usage)
}
doctor := subcommandByName(t, agent, "doctor")
for _, want := range []string{"chat", "gateway", "registration", "provider", "inspect", "after micro run"} {
if !strings.Contains(doctor.Usage, want) {
t.Fatalf("micro agent doctor usage should advertise after-run recovery for %q; usage was %q", want, doctor.Usage)
}
}
demo := subcommandByName(t, agent, "demo")
out.Reset()
if err := demo.Action(cli.NewContext(app, nil, nil)); err != nil {
@@ -88,8 +125,9 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
"No-secret first-agent demo",
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
"provider-free",
"micro agent preflight",
"micro agent preflight # before micro run: prerequisites",
"micro chat",
"micro agent doctor # after micro run: chat/gateway/inspect recovery",
"micro inspect agent <name>",
"your-first-agent.html",
"debugging-agents.html",
+29 -1
View File
@@ -1,6 +1,8 @@
package main
import (
"bytes"
"strings"
"testing"
microcmd "go-micro.dev/v6/cmd"
@@ -19,7 +21,7 @@ func TestZeroToHeroCLIBoundaries(t *testing.T) {
}
}
for _, want := range []string{"run", "chat", "flow", "inspect", "deploy"} {
for _, want := range []string{"run", "chat", "flow", "inspect", "deploy", "zero-to-hero"} {
if !commands[want] {
t.Fatalf("missing %q command", want)
}
@@ -48,3 +50,29 @@ func TestZeroToHeroCLIBoundaries(t *testing.T) {
t.Fatal("missing deploy boundary: deploy --dry-run")
}
}
func TestZeroToHeroCommandPrintsMaintainedNoSecretPath(t *testing.T) {
app := microcmd.DefaultCmd.App()
var out bytes.Buffer
oldWriter := app.Writer
app.Writer = &out
t.Cleanup(func() { app.Writer = oldWriter })
if err := app.Run([]string{"micro", "zero-to-hero"}); err != nil {
t.Fatalf("micro zero-to-hero failed: %v", err)
}
got := out.String()
for _, want := range []string{
"0→hero no-secret lifecycle demo",
"./internal/harness/zero-to-hero-ci/run.sh",
"go run ./examples/first-agent",
"go run ./examples/support",
"make harness",
"services → agents → workflows",
} {
if !strings.Contains(got, want) {
t.Fatalf("micro zero-to-hero output missing %q:\n%s", want, got)
}
}
}
+14 -6
View File
@@ -14,8 +14,10 @@ import (
type config struct {
// the current values
vals reader.Values
exit chan bool
vals reader.Values
exit chan bool
closeMu sync.Mutex
closed bool
// the current snapshot
snap *loader.Snapshot
opts Options
@@ -48,6 +50,9 @@ func (c *config) Init(opts ...Option) error {
Reader: json.NewReader(),
}
c.exit = make(chan bool)
c.closeMu.Lock()
c.closed = false
c.closeMu.Unlock()
for _, o := range opts {
o(&c.opts)
}
@@ -184,12 +189,15 @@ func (c *config) Sync() error {
}
func (c *config) Close() error {
select {
case <-c.exit:
c.closeMu.Lock()
defer c.closeMu.Unlock()
if c.closed {
return nil
default:
close(c.exit)
}
close(c.exit)
c.closed = true
return nil
}
+25
View File
@@ -6,6 +6,7 @@ import (
"path/filepath"
"runtime"
"strings"
"sync"
"testing"
"time"
@@ -45,6 +46,30 @@ func createFileForTest(t *testing.T) *os.File {
return fh
}
func TestConfigCloseConcurrentIdempotent(t *testing.T) {
conf, err := NewConfig(WithWatcherDisabled())
if err != nil {
t.Fatalf("Expected no error but got %v", err)
}
const goroutines = 64
var wg sync.WaitGroup
wg.Add(goroutines)
for i := 0; i < goroutines; i++ {
go func() {
defer wg.Done()
if err := conf.Close(); err != nil {
t.Errorf("Expected close to be idempotent but got %v", err)
}
}()
}
wg.Wait()
if err := conf.Close(); err != nil {
t.Fatalf("Expected repeated close to be idempotent but got %v", err)
}
}
func TestConfigLoadWithGoodFile(t *testing.T) {
fh := createFileForTest(t)
path := fh.Name()
+11 -6
View File
@@ -18,8 +18,10 @@ import (
type memory struct {
// the current values
vals reader.Values
exit chan bool
vals reader.Values
exit chan bool
closeMu sync.Mutex
closed bool
// the current snapshot
snap *loader.Snapshot
@@ -270,12 +272,15 @@ func (m *memory) Sync() error {
}
func (m *memory) Close() error {
select {
case <-m.exit:
m.closeMu.Lock()
defer m.closeMu.Unlock()
if m.closed {
return nil
default:
close(m.exit)
}
close(m.exit)
m.closed = true
return nil
}
+3 -1
View File
@@ -12,9 +12,11 @@ of reading the directories alphabetically.
## Recommended first-agent path
This path is the canonical services → agents → workflows route through the examples map. Debugging and observability wayfinding stays nearby once the first run works.
| Step | Start here | What you learn | Next step |
|------|------------|----------------|-----------|
| 1. First service | [`hello-world`](./hello-world/) | Create and register a basic RPC service, add a handler, call it with a client, and expose health checks. | Move to [`agent-demo`](./agent-demo/) to see services used by an agent. |
| 1. First service | [`hello-world`](./hello-world/) | Build the 0→1 service path: create and register a basic RPC service, add a handler, call it with a client, and expose health checks. | Move to [`agent-demo`](./agent-demo/) to see services used by an agent. |
| 2. First agent | [`first-agent`](./first-agent/) | Run the smallest service-backed agent with a deterministic mock model and no provider key. | Compare with [`agent-demo`](./agent-demo/) or the maintained 0-to-hero path in [`support`](./support/). |
| 3. First workflow | [`support`](./support/) | Follow typed services into an agent chat loop, an event-driven `intake` flow, and an approval gate in one runnable reference. | Deepen the workflow model with [`flow-durable`](./flow-durable/). |
+1 -1
View File
@@ -43,7 +43,7 @@ contract.
## Scheduled CI
The daily/manual `Harness (E2E)` workflow runs the same matrix with
The hourly/manual `Harness (E2E)` workflow runs the same matrix with
`GO_MICRO_AGENT_CONFORMANCE_LIVE=1` and the provider secrets exported. Providers
whose keys are absent still skip cleanly, while any configured provider must pass
the shared tool-calling scenario. This keeps scheduled conformance key-gated: PR
+41 -9
View File
@@ -157,7 +157,7 @@ func (s *NotifyService) Send(ctx context.Context, req *SendRequest, rsp *SendRes
if s.bySend == nil {
s.bySend = map[string]bool{}
}
key := strings.ToLower(strings.TrimSpace(req.To)) + "\x00" + strings.ToLower(strings.TrimSpace(req.Message))
key := notifyDedupKey(req.To, req.Message)
s.attempts++
if !s.bySend[key] {
s.bySend[key] = true
@@ -184,6 +184,25 @@ func (s *NotifyService) duplicateAttempts() int {
return s.duplicates
}
func notifyDedupKey(to, message string) string {
recipient := strings.ToLower(strings.TrimSpace(to))
body := normalizeNotifyText(message)
if recipient == "owner@acme.com" && isLaunchReadinessNotify(body) {
body = "launch-readiness"
}
return recipient + "\x00" + body
}
func normalizeNotifyText(message string) string {
return strings.Join(strings.Fields(strings.ToLower(strings.TrimSpace(message))), " ")
}
func isLaunchReadinessNotify(message string) bool {
return strings.Contains(message, "launch") &&
strings.Contains(message, "plan") &&
(strings.Contains(message, "ready") || strings.Contains(message, "readiness"))
}
// ---------------------------------------------------------------------------
// mock LLM provider — the ONLY fake. It "reasons" by simple heuristics
// over the tools it's offered and the system prompt it's given, calling
@@ -199,6 +218,10 @@ type mockModel struct {
// still keeping the regression deterministic and keyless.
unknownDelegateOnce bool
emittedUnknownDelegate bool
// duplicateNotify makes the comms mock replay the same notification call.
// The notify service should collapse that replay to one durable side effect.
duplicateNotify bool
}
func newMock(opts ...ai.Option) ai.Model {
@@ -213,6 +236,12 @@ func newMockUnknownDelegate(opts ...ai.Option) ai.Model {
return m
}
func newMockDuplicateNotify(opts ...ai.Option) ai.Model {
m := &mockModel{duplicateNotify: true}
_ = m.Init(opts...)
return m
}
func (m *mockModel) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&m.opts)
@@ -254,10 +283,14 @@ func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.Gener
// comms agent: owns notify, has Send but not Add.
case hasSend && !hasAdd:
send := findTool(req.Tools, "Send")
m.call("comms", send, map[string]any{
input := map[string]any{
"to": "owner@acme.com",
"message": "The launch plan is ready",
})
}
m.call("comms", send, input)
if m.duplicateNotify {
m.call("comms", send, input)
}
return &ai.Response{Answer: "Notified owner@acme.com."}, nil
// conductor: has the task Add tool — plan, create tasks, delegate.
@@ -322,6 +355,8 @@ func runPlanDelegate(provider string) error {
ai.Register("mock", newMock)
case "mock-unknown-delegate":
ai.Register("mock-unknown-delegate", newMockUnknownDelegate)
case "mock-duplicate-notify":
ai.Register("mock-duplicate-notify", newMockDuplicateNotify)
default:
apiKey = providerKey(provider)
if apiKey == "" {
@@ -507,8 +542,8 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
}
return nil
case <-ticker.C:
if dup := notifySvc.duplicateAttempts(); dup > 0 {
return fmt.Errorf("duplicate notify attempts: got %d duplicate replay(s), want 0", dup)
if notifySvc.count() == 1 {
continue
}
}
}
@@ -520,9 +555,6 @@ func waitForNotifySideEffect(notifySvc *NotifyService, timeout time.Duration) (b
if notifySvc.count() == 1 {
return true, nil
}
if dup := notifySvc.duplicateAttempts(); dup > 0 {
return false, fmt.Errorf("duplicate notify attempts: got %d duplicate replay(s), want 0", dup)
}
if !time.Now().Before(deadline) {
return false, nil
}
@@ -540,7 +572,7 @@ func isClientTimeout(err error) bool {
}
func main() {
provider := flag.String("provider", "mock", "LLM provider: mock (default), mock-unknown-delegate, anthropic, openai, gemini, groq, mistral, together, atlascloud")
provider := flag.String("provider", "mock", "LLM provider: mock (default), mock-unknown-delegate, mock-duplicate-notify, anthropic, openai, gemini, groq, mistral, together, atlascloud")
flag.Parse()
if err := runPlanDelegate(*provider); err != nil {
+30 -17
View File
@@ -231,6 +231,15 @@ func TestPlanDelegateRetriesAfterUnknownDelegateTool(t *testing.T) {
}
}
func TestPlanDelegateIdempotentDuplicateNotifyReplay(t *testing.T) {
if testing.Short() {
t.Skip("0→hero harness boots an end-to-end system; skipped with -short")
}
if err := runPlanDelegate("mock-duplicate-notify"); err != nil {
t.Fatalf("0→hero harness with duplicate notify replay: %v", err)
}
}
func TestTaskServiceAddIsIdempotentForLaunchTitles(t *testing.T) {
svc := new(TaskService)
for _, title := range []string{"Design", "design task", "Build", "Build launch task", "Ship", "ship readiness"} {
@@ -247,7 +256,7 @@ func TestTaskServiceAddIsIdempotentForLaunchTitles(t *testing.T) {
}
}
func TestPlanDelegateExecutionReportsDuplicateNotifyBeforeTimeout(t *testing.T) {
func TestPlanDelegateExecutionAcceptsDuplicateNotifyReplay(t *testing.T) {
notifySvc := new(NotifyService)
for i := 0; i < 2; i++ {
var rsp SendResponse
@@ -256,20 +265,16 @@ func TestPlanDelegateExecutionReportsDuplicateNotifyBeforeTimeout(t *testing.T)
}
}
done := make(chan error)
errCh := make(chan error, 1)
go func() { errCh <- waitForPlanDelegateExecution(done, new(TaskService), notifySvc) }()
select {
case err := <-errCh:
if err == nil {
t.Fatal("waitForPlanDelegateExecution returned nil, want duplicate notify error")
}
if got := err.Error(); !strings.Contains(got, "duplicate notify attempts") {
t.Fatalf("error = %q, want duplicate notify attempts", got)
}
case <-time.After(time.Second):
t.Fatal("waitForPlanDelegateExecution did not report duplicate notify before timeout")
done := make(chan error, 1)
done <- nil
if err := waitForPlanDelegateExecution(done, new(TaskService), notifySvc); err != nil {
t.Fatalf("waitForPlanDelegateExecution returned %v, want duplicate replay accepted", err)
}
if got := notifySvc.count(); got != 1 {
t.Fatalf("notify count = %d, want 1 after duplicate replay", got)
}
if got := notifySvc.duplicateAttempts(); got != 1 {
t.Fatalf("duplicate attempts = %d, want 1 recorded replay", got)
}
}
@@ -417,9 +422,14 @@ func TestPlanDelegateExecutionClassifiesPartialClientTimeout(t *testing.T) {
func TestNotifyServiceSendIsIdempotentForDuplicateDelivery(t *testing.T) {
svc := new(NotifyService)
for i := 0; i < 3; i++ {
messages := []string{
"The launch plan is ready",
"The launch plan is ready.",
"Launch readiness: the plan is ready!",
}
for i, message := range messages {
var rsp SendResponse
if err := svc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp); err != nil {
if err := svc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: message}, &rsp); err != nil {
t.Fatalf("Send attempt %d: %v", i+1, err)
}
if !rsp.Sent {
@@ -429,4 +439,7 @@ func TestNotifyServiceSendIsIdempotentForDuplicateDelivery(t *testing.T) {
if got := svc.count(); got != 1 {
t.Fatalf("notify count = %d, want 1 after duplicate delivery replays", got)
}
if got := svc.duplicateAttempts(); got != len(messages)-1 {
t.Fatalf("duplicate notify attempts = %d, want %d", got, len(messages)-1)
}
}
@@ -69,7 +69,7 @@ go run ./internal/harness/provider-conformance \
## Scheduled CI behavior
The `Harness (E2E)` workflow runs on pushes and pull requests with deterministic
mock LLMs, including `provider-conformance -providers mock`. On the daily
mock LLMs, including `provider-conformance -providers mock`. On the hourly
schedule and manual dispatch it also runs the live provider conformance job. A
manual dispatch can narrow `providers` or `harnesses`, and can set
`require_configured=true` to fail fast when an expected repository secret is
@@ -0,0 +1,43 @@
package main
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestHarnessWorkflowSchedulesLiveProviderMatrix(t *testing.T) {
path := filepath.Join(repoRoot(), ".github", "workflows", "harness.yml")
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read harness workflow: %v", err)
}
workflow := string(b)
checks := []string{
`name: Harness (E2E)`,
`schedule:`,
`cron: "17 * * * *"`,
`workflow_dispatch:`,
`harness-live:`,
`if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'`,
`ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}`,
`OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}`,
`GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}`,
`GROQ_API_KEY: ${{ secrets.GROQ_API_KEY }}`,
`MINIMAX_API_KEY: ${{ secrets.MINIMAX_API_KEY }}`,
`MISTRAL_API_KEY: ${{ secrets.MISTRAL_API_KEY }}`,
`TOGETHER_API_KEY: ${{ secrets.TOGETHER_API_KEY }}`,
`ATLASCLOUD_API_KEY: ${{ secrets.ATLASCLOUD_API_KEY }}`,
`-summary-json provider-conformance-summary.json`,
`-summary-markdown provider-conformance-summary.md`,
`-capabilities-markdown provider-capabilities.md`,
`actions/upload-artifact@v4`,
}
for _, want := range checks {
if !strings.Contains(workflow, want) {
t.Fatalf("harness workflow missing %q", want)
}
}
}
+5 -3
View File
@@ -11,9 +11,11 @@ scripted so CI can run it on every push without external services or model keys.
documented first-agent walkthrough path.
2. **Run**`micro run` remains available as the local development entry point.
3. **Chat**`micro chat` remains available as the interactive agent entry point.
4. **Inspect**`micro inspect agent <name>` and `micro inspect flow <name>`
remain available as the local run-history inspection step, with `micro flow
runs` preserving durable workflow history inspection.
4. **Inspect/debugging**`micro inspect agent <name>`, `micro agent history <name>`,
and `micro inspect flow <name>` remain available as the local run-history
inspection step. The no-secret debugging smoke seeds durable agent run history
and memory, then runs the documented inspect/history commands without provider
credentials; `micro flow runs` preserves durable workflow history inspection.
5. **Deploy**`micro deploy --dry-run <target>` remains available as the
deployment-boundary checkpoint. The dry run resolves configured deploy targets
and services and prints the remote build/copy/systemd/health plan without
@@ -1,10 +1,17 @@
package zerotoheroci
import (
"encoding/json"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"testing"
"time"
goagent "go-micro.dev/v6/agent"
"go-micro.dev/v6/store"
)
func TestZeroToHeroReferenceDocs(t *testing.T) {
@@ -20,6 +27,7 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
"./internal/harness/zero-to-hero-ci/run.sh",
"micro zero-to-hero",
"go run ./internal/harness/agent-flow",
"make provider-conformance-mock",
"internal/harness/plan-delegate",
@@ -75,6 +83,111 @@ func TestGuidesNavigationLeadsWithDoing(t *testing.T) {
}
}
func TestYourFirstAgentTutorialSmoke(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
absRoot, err := filepath.Abs(root)
if err != nil {
t.Fatalf("resolve repository root: %v", err)
}
guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "your-first-agent.md"))
for _, want := range []string{
"go test ./internal/harness/zero-to-hero-ci -run TestYourFirstAgentTutorialSmoke -count=1",
"micro agent preflight",
"mkdir first-agent",
"go mod init example.com/first-agent",
"go get go-micro.dev/v6@v6",
"micro run",
"micro call task TaskService.Create",
"micro call task TaskService.List",
"micro chat assistant",
"micro inspect agent assistant",
} {
if !strings.Contains(guide, want) {
t.Fatalf("Your First Agent guide missing copy/paste boundary %q", want)
}
}
mainGo := extractFirstAgentMain(t, guide)
workspace := t.TempDir()
writeFile(t, filepath.Join(workspace, "go.mod"), "module example.com/first-agent\n\ngo 1.24\n\nrequire go-micro.dev/v6 v6.0.0\n\nreplace go-micro.dev/v6 => "+absRoot+"\n")
writeFile(t, filepath.Join(workspace, "main.go"), mainGo)
runInWorkspace(t, workspace, "go", "mod", "tidy")
runInWorkspace(t, workspace, "go", "test", "./...")
}
func extractFirstAgentMain(t *testing.T, guide string) string {
t.Helper()
start := strings.Index(guide, "Add `main.go`:")
if start == -1 {
t.Fatal("Your First Agent guide is missing the main.go section")
}
rest := guide[start:]
open := strings.Index(rest, "```go")
if open == -1 {
t.Fatal("Your First Agent guide is missing a Go code fence for main.go")
}
rest = rest[open+len("```go"):]
close := strings.Index(rest, "```")
if close == -1 {
t.Fatal("Your First Agent guide main.go code fence is not closed")
}
return strings.TrimSpace(rest[:close]) + "\n"
}
func writeFile(t *testing.T, name, contents string) {
t.Helper()
if err := os.WriteFile(name, []byte(contents), 0o644); err != nil {
t.Fatalf("write %s: %v", name, err)
}
}
func runInWorkspace(t *testing.T, workspace, name string, args ...string) {
t.Helper()
cmd := exec.Command(name, args...)
cmd.Dir = workspace
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("Your First Agent tutorial command %q does not pass from a clean workspace: %v\n%s", strings.Join(append([]string{name}, args...), " "), err, out)
}
}
func TestArchitectureDocsAlignWithAgentHarnessLifecycle(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
doc := readFile(t, filepath.Join(root, "internal", "website", "docs", "architecture.md"))
for _, want := range []string{
"services → agents → workflows lifecycle",
"## Service substrate",
"## Agent harness",
"## Workflows",
"## Interop gateways",
"`model` / `ai.Model`",
"`store` / memory",
"`ai.Tools`",
"`agent`",
"`flow`",
"`micro mcp`",
"`micro a2a`",
"[AI Integration](ai-integration.html)",
"[Your First Agent](guides/your-first-agent.html)",
"[0→hero Reference](guides/zero-to-hero.html)",
} {
if !strings.Contains(doc, want) {
t.Fatalf("architecture doc missing lifecycle marker %q", want)
}
}
assertOrderedMarkers(t, "architecture lifecycle", doc, []string{
"## Service substrate",
"## Agent harness",
"## Workflows",
"## Interop gateways",
"## Developer path",
})
}
func TestFirstAgentWayfindingDocs(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
checks := []struct {
@@ -88,7 +201,9 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
file: filepath.Join(root, "README.md"),
heading: "### First agent on-ramp",
links: []string{
"internal/website/docs/guides/install-troubleshooting.md",
"micro agent demo",
"micro zero-to-hero",
"internal/website/docs/guides/no-secret-first-agent.md",
"internal/website/docs/guides/your-first-agent.md",
"internal/website/docs/guides/debugging-agents.md",
@@ -130,7 +245,9 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
heading: "### First-agent on-ramp",
links: []string{
"guides/install-troubleshooting.html",
"micro agent demo",
"micro zero-to-hero",
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"guides/debugging-agents.html",
@@ -148,6 +265,7 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
if idx == -1 {
t.Fatalf("%s missing first-agent wayfinding link %q; keep the no-secret → first-agent → debugging → 0→hero path discoverable", check.name, link)
}
assertWayfindingTargetExists(t, root, check.file, link)
if idx < last {
t.Fatalf("%s link %q appeared out of order; expected no-secret → first-agent → debugging → 0→hero", check.name, link)
}
@@ -157,6 +275,257 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
}
}
func TestFirstAgentWayfindingLinkTargetsResolve(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
checks := []struct {
name string
file string
heading string
}{
{
name: "README first-agent on-ramp",
file: filepath.Join(root, "README.md"),
heading: "### First agent on-ramp",
},
{
name: "README examples list",
file: filepath.Join(root, "README.md"),
heading: "## Examples",
},
{
name: "repository examples index",
file: filepath.Join(root, "examples", "README.md"),
heading: "## Recommended first-agent path",
},
{
name: "website examples index",
file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"),
heading: "## Start here",
},
{
name: "website getting-started on-ramp",
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
heading: "### First-agent on-ramp",
},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
section := firstMarkdownSection(t, readFile(t, check.file), check.heading)
links := markdownLinks(section)
if len(links) == 0 {
t.Fatalf("%s has no Markdown links in %q", check.name, check.heading)
}
for _, link := range links {
assertWayfindingTargetExists(t, root, check.file, link)
}
})
}
}
func TestFirstAgentLifecycleCommandOrderIsDocumented(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
checks := []struct {
name string
file string
heading string
markers []string
}{
{
name: "0→hero guide lifecycle",
file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"),
heading: "## What the contract covers",
markers: []string{"micro new", "micro run", "micro chat", "micro inspect agent", "micro deploy --dry-run"},
},
{
name: "CLI docs lifecycle",
file: filepath.Join(root, "cmd", "micro", "cli", "cli.go"),
heading: "const docsWayfinding",
markers: []string{"micro agent demo", "micro run", "micro chat", "micro inspect agent", "deploy dry-run"},
},
{
name: "scaffold next steps",
file: filepath.Join(root, "cmd", "micro", "cli", "new", "new.go"),
heading: "func printNextSteps",
markers: []string{"go run .", "micro chat", "micro inspect agent", "micro agent demo", "micro docs"},
},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
doc := readFile(t, check.file)
if check.heading != "" {
start := strings.Index(doc, check.heading)
if start == -1 {
t.Fatalf("%s missing %q boundary", check.name, check.heading)
}
doc = doc[start:]
}
assertOrderedMarkers(t, check.name, doc, check.markers)
})
}
}
func TestExamplesIndexesPreserveLifecycleMap(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
checks := []struct {
name string
file string
heading string
want []string
ordered []string
}{
{
name: "repository examples lifecycle map",
file: filepath.Join(root, "examples", "README.md"),
heading: "## Recommended first-agent path",
want: []string{
"hello-world",
"0→1",
"first-agent",
"support",
"services",
"agents",
"workflows",
"Debugging and observability",
},
ordered: []string{"1. First service", "2. First agent", "3. First workflow"},
},
{
name: "website examples lifecycle map",
file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"),
heading: "## Start here",
want: []string{
"examples/hello-world",
"0→1",
"examples/first-agent",
"examples/support",
"services",
"agents",
"workflows",
"debugging-agents.html",
},
ordered: []string{"0→1 service", "Provider-free first agent", "0→hero lifecycle"},
},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
section := firstMarkdownSection(t, readFile(t, check.file), check.heading)
for _, want := range check.want {
if !strings.Contains(section, want) {
t.Fatalf("%s missing lifecycle map marker %q", check.name, want)
}
}
last := -1
for _, marker := range check.ordered {
idx := strings.Index(section, marker)
if idx == -1 {
t.Fatalf("%s missing ordered example marker %q", check.name, marker)
}
if idx < last {
t.Fatalf("%s marker %q appeared out of order; keep examples flowing hello-world/0→1 → first-agent → support/0→hero", check.name, marker)
}
last = idx
}
})
}
}
func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
checks := []struct {
name string
file string
section string
want []string
}{
{
name: "README quick start",
file: filepath.Join(root, "README.md"),
section: "## Quick Start",
want: []string{
"install troubleshooting guide",
"### Fastest start — no API key",
"micro new helloworld",
"micro run",
"curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call",
"### First agent on-ramp",
"micro agent demo",
"### Generate from a prompt — with an LLM key",
},
},
{
name: "CLI README",
file: filepath.Join(root, "cmd", "micro", "README.md"),
section: "## Create a service",
want: []string{
"## Create a service",
"micro new helloworld",
"## Run the service",
"micro run",
"micro agent demo",
},
},
{
name: "website getting started",
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
section: "Install troubleshooting",
want: []string{
"Install troubleshooting",
"## Quick Start: Scaffold, Run, Call",
"micro new helloworld",
"micro run",
"curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call",
"### First-agent on-ramp",
"micro agent demo",
"## Generate from a Prompt — with an LLM key",
},
},
{
name: "website quickstart",
file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"),
section: "## Create Your First Service",
want: []string{
"micro new helloworld",
"micro run",
"curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call",
"## Next Steps",
"micro agent demo",
"micro zero-to-hero",
"guides/no-secret-first-agent.html",
"guides/debugging-agents.html",
"guides/zero-to-hero.html",
},
},
}
for _, check := range checks {
t.Run(check.name, func(t *testing.T) {
doc := readFile(t, check.file)
if check.section != "" {
start := strings.Index(doc, check.section)
if start == -1 {
t.Fatalf("%s missing %q section", check.name, check.section)
}
doc = doc[start:]
}
last := -1
for _, want := range check.want {
idx := strings.Index(doc, want)
if idx == -1 {
t.Fatalf("%s missing no-secret first-run marker %q", check.name, want)
}
if idx < last {
t.Fatalf("%s marker %q appeared out of order; keep install/scaffold/run/call before provider-backed generation", check.name, want)
}
last = idx
}
})
}
}
func TestNoSecretFirstAgentTranscript(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"))
@@ -205,6 +574,110 @@ func TestNoSecretFirstAgentTranscript(t *testing.T) {
}
}
func TestNoSecretFirstAgentDebuggingSmoke(t *testing.T) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
home := t.TempDir()
storeDir := filepath.Join(home, "micro", "store")
st := store.NewFileStore(store.DirOption(storeDir))
seedNoSecretAgentDebuggingState(t, st)
if err := st.Close(); err != nil {
t.Fatalf("close seeded store: %v", err)
}
micro := buildMicroBinary(t, root)
for _, tc := range []struct {
name string
args []string
want []string
}{
{
name: "demo advertises provider-free debug path",
args: []string{"agent", "demo"},
want: []string{"No-secret first-agent demo", "provider-free", "run history", "micro inspect agent <name>"},
},
{
name: "inspect shows seeded run history",
args: []string{"inspect", "agent", "assistant", "--limit", "1"},
want: []string{`Agent "assistant" runs`, "run-debug-smoke", "status=done", "events=3", "last=done", "trace=trace-debug-"},
},
{
name: "inspect filters documented statuses",
args: []string{"inspect", "agent", "--status", "done", "--json", "assistant"},
want: []string{"run-debug-smoke", `"status": "done"`, `"trace_id": "trace-debug-smoke"`},
},
{
name: "agent history shows memory and run index",
args: []string{"agent", "history", "assistant"},
want: []string{"user:", "Triage ticket-1", "assistant:", "ticket-1 is ready", "Runs:", "run-debug-smoke", "status=done"},
},
} {
t.Run(tc.name, func(t *testing.T) {
out := runMicroCLIWithHome(t, micro, home, tc.args...)
for _, want := range tc.want {
if !strings.Contains(out, want) {
t.Fatalf("micro %s output missing %q:\n%s", strings.Join(tc.args, " "), want, out)
}
}
})
}
}
func seedNoSecretAgentDebuggingState(t *testing.T, st store.Store) {
t.Helper()
scoped := store.Scope(st, "agent", "assistant")
runID := "run-debug-smoke"
events := []goagent.RunEvent{
{Time: time.Unix(1700000000, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "run", Name: "ask"},
{Time: time.Unix(1700000001, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "model", Provider: "mock", Model: "first-agent-mock"},
{Time: time.Unix(1700000002, 0), RunID: runID, Agent: "assistant", TraceID: "trace-debug-smoke", Kind: "done", Name: "answer"},
}
for _, event := range events {
b, err := json.Marshal(event)
if err != nil {
t.Fatal(err)
}
key := "runs/" + event.RunID + "/" + event.Time.Format("20060102150405.000000000") + "-" + event.Kind
if err := scoped.Write(&store.Record{Key: key, Value: b}); err != nil {
t.Fatalf("seed run event: %v", err)
}
}
mem := goagent.NewMemory(scoped, "history", 10)
mem.Add("user", "Triage ticket-1 for Alice")
mem.Add("assistant", "ticket-1 is ready for Alice without provider secrets")
}
func buildMicroBinary(t *testing.T, root string) string {
t.Helper()
bin := filepath.Join(t.TempDir(), "micro")
cmd := exec.Command("go", "build", "-o", bin, "./cmd/micro")
cmd.Dir = root
out, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("build micro CLI failed: %v\n%s", err, out)
}
return bin
}
func runMicroCLIWithHome(t *testing.T, micro, home string, args ...string) string {
t.Helper()
cmd := exec.Command(micro, args...)
cmd.Env = 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) {
root := filepath.Clean(filepath.Join("..", "..", ".."))
for _, target := range []string{
@@ -243,3 +716,67 @@ func readFile(t *testing.T, name string) string {
}
return string(data)
}
var markdownLinkRE = regexp.MustCompile(`\[[^\]]+\]\(([^)#?]+)(?:[#?][^)]*)?\)`)
func markdownLinks(section string) []string {
matches := markdownLinkRE.FindAllStringSubmatch(section, -1)
links := make([]string, 0, len(matches))
for _, match := range matches {
if len(match) > 1 {
links = append(links, match[1])
}
}
return links
}
func assertWayfindingTargetExists(t *testing.T, root, sourceFile, link string) {
t.Helper()
if !strings.Contains(link, "/") && !strings.Contains(link, ".") {
return
}
if strings.HasPrefix(link, "http://") || strings.HasPrefix(link, "https://") {
switch {
case strings.HasPrefix(link, "https://go-micro.dev/docs/"):
link = strings.TrimPrefix(link, "https://go-micro.dev/docs/")
link = filepath.ToSlash(filepath.Join("internal", "website", "docs", strings.TrimSuffix(link, ".html")+".md"))
case strings.HasPrefix(link, "https://github.com/micro/go-micro/tree/master/"):
link = strings.TrimPrefix(link, "https://github.com/micro/go-micro/tree/master/")
default:
return
}
} else if strings.HasSuffix(link, ".html") {
sourceDir := filepath.Dir(sourceFile)
websiteDocs := filepath.Join(root, "internal", "website", "docs")
resolved := filepath.Clean(filepath.Join(sourceDir, filepath.FromSlash(link)))
if rel, err := filepath.Rel(websiteDocs, resolved); err == nil && !strings.HasPrefix(rel, "..") {
link = filepath.ToSlash(filepath.Join("internal", "website", "docs", strings.TrimSuffix(rel, ".html")+".md"))
}
} else if strings.HasPrefix(link, ".") {
target := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), filepath.FromSlash(link)))
if _, err := os.Stat(target); err != nil {
t.Fatalf("first-agent wayfinding link %q in %s resolves to missing target %s: %v", link, sourceFile, target, err)
}
return
}
target := filepath.Join(root, filepath.FromSlash(link))
if _, err := os.Stat(target); err != nil {
t.Fatalf("first-agent wayfinding link %q in %s resolves to missing target %s: %v", link, sourceFile, target, err)
}
}
func assertOrderedMarkers(t *testing.T, name, doc string, markers []string) {
t.Helper()
last := -1
for _, marker := range markers {
idx := strings.Index(doc, marker)
if idx == -1 {
t.Fatalf("%s missing lifecycle command marker %q", name, marker)
}
if idx < last {
t.Fatalf("%s marker %q appeared out of order; keep scaffold → run → chat → inspect → deploy discoverable", name, marker)
}
last = idx
}
}
+1 -1
View File
@@ -8,7 +8,7 @@ cd "$ROOT"
# without secrets or long-running daemons.
go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestZeroToHeroCLIBoundaries' -count=1
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestZeroToHeroReferenceDocs' -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|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
+3
View File
@@ -3,6 +3,8 @@ core:
url: /docs/
- title: Getting Started
url: /docs/getting-started.html
- title: Install Troubleshooting
url: /docs/guides/install-troubleshooting.html
- title: AI Integration
url: /docs/ai-integration.html
- title: No-secret First Agent
@@ -80,6 +82,7 @@ project:
- title: Server (optional)
url: /docs/server.html
search_order:
- /docs/guides/install-troubleshooting.html
- /docs/guides/your-first-agent.html
- /docs/guides/zero-to-hero.html
- /docs/guides/debugging-agents.html
+100 -51
View File
@@ -2,75 +2,124 @@
layout: default
---
## Architecture
# Architecture
<img src="/images/generated/architecture.jpg" alt="Go Micro architecture" style="width: 100%; border-radius: 8px; margin: 1rem 0 1.5rem;" />
An overview of the Go Micro architecture.
Go Micro is one runtime for the services → agents → workflows lifecycle. The same
registry, client/server RPC, store, broker, and gateway primitives that run a
service also give an agent discoverable tools, durable state, interop, and a
place to hand off deterministic work.
## Overview
## Lifecycle map
Go Micro abstracts away the details of distributed systems. Here are the main features.
```text
Services → Agents → Workflows
handlers model loop durable orchestration
registry memory triggers and ordered steps
RPC tools guardrails agent dispatch
```
- **Authentication** - Auth is built in as a first class citizen. Authentication and authorization enable secure
zero trust networking by providing every service an identity and certificates. This additionally includes rule
based access control.
The layers are progressive: start with a service, expose its endpoints as tools,
wrap those tools with an agent, then move the known paths into flows so the model
only handles the uncertain parts.
- **Dynamic Config** - Load and hot reload dynamic config from anywhere. The config interface provides a way to load application
level config from any source such as env vars, file, etcd. You can merge the sources and even define fallbacks.
## Service substrate
- **Data Storage** - A simple data store interface to read, write and delete records. It includes support for many storage backends
in the plugins repo. State and persistence becomes a core requirement beyond prototyping and Micro looks to build that into the framework.
Go Micro's service framework supplies the distributed-systems base every agent
needs:
- **Service Discovery** - Automatic service registration and name resolution. Service discovery is at the core of micro service
development. When service A needs to speak to service B it needs the location of that service. The default discovery mechanism is
multicast DNS (mdns), a zeroconf system.
- **Registry** — services, agents, and flows register under names so clients,
gateways, and other agents can discover them without hard-coded addresses. The
default is mDNS for local development, with pluggable backends for production.
- **RPC client/server** — endpoints are normal Go handlers reached through the
client, load balanced through discovery, encoded through codecs, and optionally
streamed.
- **Broker** — asynchronous events connect services and trigger flows without
coupling producers to consumers.
- **Config and auth** — dynamic configuration plus identity and authorization keep
local and production runtimes using the same shape.
- **Pluggable interfaces** — registry, broker, store, transport, codecs, auth, and
config are Go interfaces, so the runtime can stay stable while deployments swap
infrastructure.
- **Load Balancing** - Client side load balancing built on service discovery. Once we have the addresses of any number of instances
of a service we now need a way to decide which node to route to. We use random hashed load balancing to provide even distribution
across the services and retry a different node if there's a problem.
That substrate is intentionally not separate from the agent stack. A service
endpoint is the smallest useful unit of work, and the registry is the source of
truth for which tools and agents exist.
- **Message Encoding** - Dynamic message encoding based on content-type. The client and server will use codecs along with content-type
to seamlessly encode and decode Go types for you. Any variety of messages could be encoded and sent from different clients. The client
and server handle this by default. This includes protobuf and json by default.
## Agent harness
- **RPC Client/Server** - RPC based request/response with support for bidirectional streaming. We provide an abstraction for synchronous
communication. A request made to a service will be automatically resolved, load balanced, dialled and streamed.
Agents compose the service substrate with the AI-specific packages:
- **Async Messaging** - PubSub is built in as a first class citizen for asynchronous communication and event driven architectures.
Event notifications are a core pattern in micro service development. The default messaging system is a HTTP event message broker.
- **`model` / `ai.Model`** — a pluggable model interface normalizes provider calls
while letting applications pick Anthropic, OpenAI, Gemini, Atlas Cloud, Groq,
Mistral, Together AI, or a mock model for no-secret tests.
- **`store` / memory** — agent history, plans, run state, and compacted memory live
in durable storage rather than in an in-process chat loop.
- **`ai.Tools`** — discovers registered service endpoints and executes them through
the Go Micro client, so tools are generated from running services instead of a
parallel tool registry.
- **`agent`** — runs the tool-calling loop with guardrails, planning, delegation,
service-backed memory, and an `Agent.Chat` RPC endpoint. An agent is therefore a
service other clients and agents can call.
- **Pluggable Interfaces** - Go Micro makes use of Go interfaces for each distributed system abstraction. Because of this these interfaces
are pluggable and allows Go Micro to be runtime agnostic. You can plugin any underlying technology.
The result is a harness, not just a prompt loop: model calls are bounded by tool
scope, state is recoverable, and the same CLI and gateways that reach services can
reach agents.
## Design
## Workflows
We will share more on architecture soon
Use `flow` when the path is known or must be repeatable. Flows subscribe to broker
events, run ordered deterministic steps, and can dispatch to an agent at the point
where judgment or language understanding is needed. This keeps long-running work
observable and restartable while preserving agents for open-ended decisions.
A common shape is:
1. A service emits an event such as `ticket.created`.
2. A flow validates and enriches the event with deterministic handlers.
3. The flow dispatches to an agent for classification, drafting, or escalation.
4. The agent calls registered service tools and returns to the flow for final
durable steps.
## Interop gateways
Gateways project the same runtime to external callers:
- **`micro api`** exposes service RPC over HTTP.
- **`micro mcp`** exposes registered service endpoints as Model Context Protocol
tools for external agents.
- **`micro a2a`** exposes registered Go Micro agents through the Agent2Agent
protocol and lets Go Micro flows or agents dispatch to agents hosted elsewhere.
MCP is the services-as-tools boundary; A2A is the agents-as-agents boundary. Both
come from registry metadata, so adding a service or agent updates the external
surface without duplicate wiring.
## Developer path
If you are new, follow the architecture in the same order the runtime composes it:
1. [Install troubleshooting](guides/install-troubleshooting.html) — make sure the
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)
— run one service-backed agent with a mock model.
4. [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
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.
## Related
- [ADR Index](architecture/index.md)
- [AI Integration](ai-integration.html) — layer-by-layer services → agents → workflows wiring
- [Getting Started](getting-started.html) — first service and first-agent on-ramp
- [Examples](examples/) — runnable examples mapped to the lifecycle
- [ADR Index](architecture/index.md) — architecture decision records
- [Configuration](config.html)
- [Plugins](plugins.html)
## Example Usage
Here's a minimal Go Micro service demonstrating the architecture:
```go
package main
import (
"go-micro.dev/v6"
"log"
)
func main() {
service := micro.NewService("example",
)
service.Init()
if err := service.Run(); err != nil {
log.Fatal(err)
}
}
```
+59 -54
View File
@@ -17,15 +17,7 @@ Go Micro has three core abstractions:
## Prerequisites
- **Go 1.24+** for development. The `curl` install below gives you the `micro` binary without Go, but `micro run` compiles your services, so you'll want Go installed to build them.
- An **LLM provider key** (Anthropic, OpenAI, Gemini, …) *only* for the AI features — `micro run --prompt`, `micro chat`, and agents. Plain services need no key. Set it before running, e.g. `export ANTHROPIC_API_KEY=sk-ant-...`.
Before your first provider-backed agent run, check the local path with:
```bash
micro agent preflight
```
The preflight is read-only: it verifies Go 1.24+, the `micro` binary, provider-key setup, and whether the default `micro run` gateway port is free, without calling an LLM provider. When a check fails it prints the exact fix plus the next guide to open, so the scaffold → run → chat path stays walkable.
- **No LLM provider key is required** for the first run below. Add an Anthropic, OpenAI, Gemini, or other provider key only when you reach the provider-backed generation and chat steps.
## Install
@@ -37,67 +29,43 @@ curl -fsSL https://go-micro.dev/install.sh | sh
go install go-micro.dev/v6/cmd/micro@latest
```
## Quick Start: Generate from a Prompt
If install or shell setup fails, start with [Install troubleshooting](guides/install-troubleshooting.html) to verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path.
Prefer to start from a runnable reference? Clone the repository and run the maintained support-desk lifecycle example first:
## Quick Start: Scaffold, Run, Call
Start with the path that proves the runtime works before any provider setup: install the CLI, scaffold one service, run it locally, then call it through the gateway.
```bash
git clone https://github.com/micro/go-micro.git
cd go-micro
go run ./examples/support
micro new helloworld
cd helloworld
micro run
```
That example is the no-secret 0→hero path: services expose ticket/customer/notification tools, an agent handles the work, and an event-driven flow triggers the agent. See [Learn by Example](examples/) when you want more runnable starting points.
Describe what you need. The AI designs services, writes handlers, compiles, and starts them:
In another terminal, call the generated service:
```bash
micro run --prompt "task management system"
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
-H 'Content-Type: application/json' -d '{"name":"World"}'
```
You'll see the design, confirm, and services + agent start:
That install → scaffold → run → call loop is the 0→1 contract. It requires Go and the `micro` binary, but no LLM key. Once this succeeds, you know the local runtime, hot reload, gateway, and service registration are working.
```text
Services:
● task — Core task management
● project — Project organization
Generate? [Y/n]
Micro
Services:
● task
● project
Agents:
◆ agent
```
The interactive console lets you talk to your services immediately:
```text
> Create a project called Launch, then add a task called 'Write docs'
→ project_Project_Create({"name":"Launch"})
← {"record":{"id":"p1..."},"success":true}
→ task_Task_Create({"title":"Write docs","project_id":"p1..."})
Created project Launch and added task 'Write docs' to it.
```
The console discovers services from the registry and orchestrates across them via the agent. Use `micro run -d` for detached mode without the console, or `micro chat` as a standalone command.
### First-agent on-ramp
After this quick start, follow the agent path in order:
1. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
2. [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.
3. [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.
4. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
5. [Debugging your agent](guides/debugging-agents.html) — inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
6. [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`.
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. [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.
6. [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.
7. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
8. [Debugging your agent](guides/debugging-agents.html) — inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
9. [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`.
## Quick Start: Write a Service
## Write a Service
Create and run a service manually:
@@ -161,6 +129,43 @@ micro new events --template pubsub
micro new gateway --template api
```
## Generate from a Prompt — with an LLM key
After the no-secret path works, set a provider key if you want Go Micro to design services and an agent from a prompt:
```bash
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
micro run --prompt "task management system" --provider anthropic
```
You'll see the design, confirm it, and then services plus an agent start:
```text
Services:
● task — Core task management
● project — Project organization
Generate? [Y/n]
Micro
Services:
● task
● project
Agents:
◆ agent
```
Use the interactive console, `micro run -d` plus `micro chat`, or the agent playground to talk to the generated services.
Before your first provider-backed agent run, check the local path with:
```bash
micro agent preflight
```
The preflight is read-only: it verifies Go 1.24+, the `micro` binary, provider-key setup, and whether the default `micro run` gateway port is free, without calling an LLM provider. When a check fails it prints the exact fix plus the next guide to open, so the scaffold → run → chat path stays walkable.
## Building Agents
For a complete service-backed walkthrough, start with [Your First Agent](guides/your-first-agent.html). If you want to run before you write, use [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support) for the full services → agents → workflows lifecycle or [`examples/agent-plan-delegate`](https://github.com/micro/go-micro/tree/master/examples/agent-plan-delegate) for the smallest multi-agent planning/delegation path.
@@ -17,9 +17,21 @@ micro inspect ... # read the recorded run or workflow history
Debug the lifecycle in the same order Go Micro runs it: first prove the service is
registered and callable, then inspect the agent run that chose tools, then inspect
any workflow that handed off to the agent. If the first local run fails before a
chat turn, run `micro agent preflight`; failed checks include `Fix:` and `Next:`
lines for Go, CLI installation, provider-key setup, and the local gateway port.
any workflow that handed off to the agent.
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) |
| 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
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
settings, registered agents, provider configuration, and inspectable run history.
## 1. Reproduce one small turn
@@ -0,0 +1,96 @@
---
layout: default
title: Install troubleshooting
---
# Install troubleshooting
Use this page before `micro new` or `micro agent demo` when the CLI install is
unclear. The goal is to prove three boundaries in order: the `micro` binary is on
`PATH`, it is the version you expected, and the no-secret first-run path works
without provider keys.
## 1. Choose one install path
### Binary installer (no Go required to install)
```sh
curl -fsSL https://go-micro.dev/install.sh | sh
```
Use this when you want the released `micro` binary without building it yourself.
The generated services still need a Go toolchain when you run `micro run`, but the
installer itself does not require Go.
### Go install (build from source)
```sh
go install go-micro.dev/v6/cmd/micro@latest
```
Use this when Go is already installed and you want the binary in your Go bin
directory. If the command succeeds but `micro` is not found, your Go bin directory
is probably not on `PATH`.
## 2. Verify `PATH` and version
Check which binary your shell will run:
```sh
command -v micro
micro --version
```
If `command -v micro` prints nothing, add the install directory to `PATH`, then
open a new terminal and retry. Common locations are:
```sh
export PATH="$HOME/.micro/bin:$PATH" # binary installer
export PATH="$(go env GOPATH)/bin:$PATH" # go install
```
If `micro --version` shows an older binary than expected, remove the stale copy or
put the intended install directory earlier in `PATH`.
## 3. Run the no-secret smoke path
Once `micro` resolves, prove the local service runtime before adding LLM provider
keys:
```sh
micro new helloworld
cd helloworld
micro run
```
In another terminal:
```sh
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
-H 'Content-Type: application/json' -d '{"name":"World"}'
```
This checks the scaffold, local build, gateway, and service registration without
calling a model provider.
## 4. Recover common failures
| Symptom | Check | Fix |
|---------|-------|-----|
| `micro: command not found` | `command -v micro` | Add the installer bin directory or `$(go env GOPATH)/bin` to `PATH`, then open a new terminal. |
| `micro run` cannot find Go | `go version` | Install Go 1.24 or newer from <https://go.dev/doc/install>. |
| The gateway port is busy | `lsof -i :8080` | Stop the process using the port, or run with a different address. |
| Provider-key errors block an agent run | `micro agent preflight` | Stay on the no-secret path first: run `micro agent demo`, then the no-secret first-agent guide. |
## 5. Continue the first-agent on-ramp
After install verification succeeds, continue in order:
1. `micro agent demo` — print the provider-free first-agent demo command and next docs steps.
2. [No-secret first-agent transcript](no-secret-first-agent.html) — prove an agent can use services without a provider key.
3. [Your First Agent](your-first-agent.html) — build and chat with your own service-backed agent.
4. [Debugging your agent](debugging-agents.html) — inspect registration, tool calls, run history, and provider failures.
5. [0→hero Reference](zero-to-hero.html) — walk the full services → agents → workflows lifecycle.
For repository contributors, `make install-smoke` runs the same installer seam
against a local build without network access.
@@ -100,6 +100,7 @@ CI keeps those CLI boundaries present with:
```sh
go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1
go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1
```
## Debug transcript checkpoint
@@ -47,13 +47,17 @@ export ANTHROPIC_API_KEY=sk-ant-...
Plain service calls work without a model key; the key is only needed when the
agent reasons over tools.
Run the read-only first-agent preflight before starting the walkthrough. The same CLI boundary is covered by CI with `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1`, so the documented scaffold → run → chat → inspect path stays visible in the local harness:
Run the read-only first-agent preflight before starting the walkthrough. The same CLI boundary is covered by CI with `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1`, and the copy/paste tutorial code is built from a clean temporary workspace with `go test ./internal/harness/zero-to-hero-ci -run TestYourFirstAgentTutorialSmoke -count=1`, so the documented scaffold → run → chat → inspect path stays visible in the local harness:
```sh
micro agent preflight
```
It checks Go 1.24+, the `micro` binary, provider-key setup, and the default local gateway port without contacting a provider. Failed checks include a `Fix:` line and a `Next:` line that points back to this guide, the no-secret walkthrough, or the debugging guide.
It checks Go 1.24+, the `micro` binary, provider-key setup, and the default local gateway port without contacting a provider. Failed checks include a `Fix:` line and a `Next:` line that points back to this guide, the no-secret walkthrough, or the debugging guide. Use it before `micro run`; if `micro run` is already active but `micro chat`, the `/agent` gateway, registration, provider settings, or inspect history is failing, run the after-run recovery check instead:
```sh
micro agent doctor
```
## 1. Create a workspace
@@ -173,7 +177,14 @@ Create a task called "Review the first-agent walkthrough", then show me all task
```
A healthy run shows the agent calling the task service and then summarizing the
result. If the model refuses to call tools, tighten the prompt so it explicitly
result. Inspect the recorded run when you want to see the tool calls, memory,
and timing behind the answer:
```sh
micro inspect agent assistant
```
If the model refuses to call tools, tighten the prompt so it explicitly
uses the `task` service before answering.
## 4. Know what just happened
+12 -2
View File
@@ -19,15 +19,25 @@ cloud credentials?"
| --- | --- | --- |
| Scaffold | `micro new` generates a runnable service with and without MCP support. | `go test ./cmd/micro/cli/new -run TestZeroToOne -count=1` |
| First-agent wayfinding | README and the website getting-started docs keep the no-secret → first-agent → debugging → 0→hero links present and in order. | `go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfindingDocs -count=1` |
| First agent | `micro new`, `micro agent preflight`, `micro run`, `micro chat`, and `micro inspect agent` stay available for the documented first-agent walkthrough. | `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1` |
| First agent | `micro new`, `micro agent preflight`, `micro run`, `micro chat`, and `micro inspect agent <name>` stay available for the documented first-agent walkthrough. | `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1` |
| Run | `micro run` remains the local development entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
| Chat | `micro chat` remains the interactive agent entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
| Inspect | `micro inspect agent`, `micro inspect flow`, and `micro flow runs` remain discoverable for run history. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
| 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` resolves deploy targets without touching remote infrastructure. | `go test ./cmd/micro/cli/deploy -run TestDeployDryRun -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 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
After installing the CLI, ask `micro` for the maintained no-secret lifecycle command:
```sh
micro zero-to-hero
```
The command prints the exact harness command below plus the smaller runnable examples, so a new developer can discover the 0→hero path from CLI help instead of translating this guide by hand.
## Run the runnable example
From the repository root, start with the smallest service-backed agent when you want the fastest no-secret success path:
+2 -1
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: `micro agent demo` for the installed no-secret CLI affordance, [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 inspect 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: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for copy/pasteable runnable examples, [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 inspect 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.
@@ -25,6 +25,7 @@ Otherwise continue to read the docs for more information about the framework.
- [Getting Started](getting-started.html)
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → inspect → deploy dry-run with CI-backed commands
- `micro agent demo` - Show the provider-free first-agent demo command and next docs steps
- `micro examples` - Show provider-free first-agent examples in copy/paste order
- [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
- [MCP & AI Agents](mcp.html) - Turn services into AI-callable tools with the Model Context Protocol
+11 -5
View File
@@ -16,6 +16,8 @@ Or, if you have Go and prefer to build from source:
go install go-micro.dev/v6/cmd/micro@latest
```
If the installer finishes but your shell cannot find `micro`, open [Install troubleshooting](guides/install-troubleshooting.html) before creating your first service.
## Create Your First Service
```bash
@@ -39,10 +41,15 @@ curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
You now have the service half of the services → agents → workflows lifecycle running locally. Keep the on-ramp going in this order:
1. **[Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)** - run a mock-model, no-secret agent before adding provider keys.
2. **[Your First Agent](guides/your-first-agent.html)** - turn this service into an agent-callable tool, chat with it, and learn the `micro agent preflight``micro run``micro chat` loop.
3. **[Debugging your agent](guides/debugging-agents.html)** - inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent does something surprising.
4. **[0→hero Reference](guides/zero-to-hero.html)** - walk the maintained scaffold → run → chat → inspect → deploy dry-run path that proves services, agents, and workflows together.
1. **[Install troubleshooting](guides/install-troubleshooting.html)** - verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path.
2. `micro agent demo` - print the provider-free first-agent demo command and the 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. **[Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)** - run a mock-model, no-secret agent before adding provider keys.
6. **[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.
7. **[Your First Agent](guides/your-first-agent.html)** - turn this service into an agent-callable tool, chat with it, and learn the `micro agent preflight``micro run``micro chat` loop.
8. **[Debugging your agent](guides/debugging-agents.html)** - inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent does something surprising.
9. **[0→hero Reference](guides/zero-to-hero.html)** - walk the maintained scaffold → run → chat → inspect → deploy dry-run path that proves services, agents, and workflows together.
After that first-agent path, branch out to:
@@ -113,4 +120,3 @@ publisher.Publish(ctx, &UserCreatedEvent{
- **[Discord Community](https://discord.gg/G8Gk5j3uXr)** - Chat with other users
- **[GitHub Issues](https://github.com/micro/go-micro/issues)** - Report bugs or request features
- **[Documentation](https://go-micro.dev/docs/)** - Complete docs