Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d5dd42f6f9 |
@@ -21,8 +21,8 @@ changes, architectural rewrites. Those go to the human.
|
||||
|
||||
## Work queue (ranked)
|
||||
|
||||
1. **Make plan-delegate harness complete delegated notification steps** ([#4138](https://github.com/micro/go-micro/issues/4138)) — the duplicate-notification fix and first-agent tutorial harness have both shipped, but the live atlascloud plan-delegate matrix still exposed a higher-severity Now-phase contract break: a delegated notification can succeed or be reused while the persisted plan step remains incomplete. Fixing that keeps the provider conformance evaluator trustworthy without relaxing the services → agents → workflows lifecycle gate.
|
||||
2. **Verify no-secret agent debugging walkthrough** ([#4142](https://github.com/micro/go-micro/issues/4142)) — keep adoption pressure balanced with hardening by extending the maintained 0→1 path past scaffold/run/chat into inspect and debugging. A provider-free smoke check for the documented first-agent debug sequence will catch command drift at the exact seam where new developers need confidence after the first conversation behaves unexpectedly.
|
||||
1. **Stabilize plan-delegate harness against duplicate delegated notifications** ([#4118](https://github.com/micro/go-micro/issues/4118)) — the scheduled provider-conformance matrix is now in place, and its first live signal exposed an atlascloud/minimax duplicate-notify regression. Preserve the semantic “exactly one launch-readiness notification” contract with focused regression coverage so the live matrix remains a useful evaluator rather than a noisy gate.
|
||||
2. **Add a copy/paste first-agent tutorial smoke harness** ([#4128](https://github.com/micro/go-micro/issues/4128)) — the CLI examples wayfinding shipped, so keep adoption pressure on the next most valuable seam: prove the website’s Your First Agent path can be followed from a clean workspace without relying on prose staying honest by hand. A focused CI-verifiable tutorial boundary keeps scaffold → run → chat → inspect cohesive after future CLI and docs changes.
|
||||
|
||||
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
|
||||
architecture-review pass._
|
||||
|
||||
@@ -16,40 +16,6 @@ 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
|
||||
|
||||
+2
-23
@@ -400,7 +400,7 @@ func preserveCompletedPlanSteps(stored string, input map[string]any) map[string]
|
||||
continue
|
||||
}
|
||||
task, _ := step["task"].(string)
|
||||
if completed[planTaskCompletionKey(task)] && isUnfinishedPlanStatus(step["status"]) {
|
||||
if completed[normalizePlanTask(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 = planTaskCompletionKey(task); task != "" {
|
||||
if task = normalizePlanTask(task); task != "" {
|
||||
completed[task] = true
|
||||
}
|
||||
}
|
||||
@@ -434,27 +434,6 @@ 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"
|
||||
|
||||
@@ -79,27 +79,6 @@ 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)
|
||||
|
||||
@@ -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 := notifyDedupKey(req.To, req.Message)
|
||||
key := strings.ToLower(strings.TrimSpace(req.To)) + "\x00" + strings.ToLower(strings.TrimSpace(req.Message))
|
||||
s.attempts++
|
||||
if !s.bySend[key] {
|
||||
s.bySend[key] = true
|
||||
@@ -184,25 +184,6 @@ 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
|
||||
|
||||
@@ -422,14 +422,9 @@ func TestPlanDelegateExecutionClassifiesPartialClientTimeout(t *testing.T) {
|
||||
|
||||
func TestNotifyServiceSendIsIdempotentForDuplicateDelivery(t *testing.T) {
|
||||
svc := new(NotifyService)
|
||||
messages := []string{
|
||||
"The launch plan is ready",
|
||||
"The launch plan is ready.",
|
||||
"Launch readiness: the plan is ready!",
|
||||
}
|
||||
for i, message := range messages {
|
||||
for i := 0; i < 3; i++ {
|
||||
var rsp SendResponse
|
||||
if err := svc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: message}, &rsp); err != nil {
|
||||
if err := svc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp); err != nil {
|
||||
t.Fatalf("Send attempt %d: %v", i+1, err)
|
||||
}
|
||||
if !rsp.Sent {
|
||||
@@ -439,7 +434,4 @@ 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ package zerotoheroci
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
@@ -78,76 +77,6 @@ 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"))
|
||||
|
||||
@@ -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|TestYourFirstAgentTutorialSmoke' -count=1
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestZeroToHeroReferenceDocs' -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
|
||||
|
||||
@@ -47,7 +47,7 @@ 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`, 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:
|
||||
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:
|
||||
|
||||
```sh
|
||||
micro agent preflight
|
||||
@@ -177,14 +177,7 @@ 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. 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
|
||||
result. If the model refuses to call tools, tighten the prompt so it explicitly
|
||||
uses the `task` service before answering.
|
||||
|
||||
## 4. Know what just happened
|
||||
|
||||
Reference in New Issue
Block a user