Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b21f8a47aa |
@@ -21,8 +21,8 @@ changes, architectural rewrites. Those go to the human.
|
||||
|
||||
## Work queue (ranked)
|
||||
|
||||
1. **Propagate cancellation and retry signals through provider model calls** ([#4175](https://github.com/micro/go-micro/issues/4175)) — the installed first-agent on-ramp shipped in #4207 and AtlasCloud guarded-delegate conformance closed in #4211, so the highest open Now-phase risk is failure handling under real provider conditions: cancellation/deadline propagation and retry/backoff must not duplicate tool side effects. This keeps the services → agents → workflows lifecycle dependable across providers without changing public APIs.
|
||||
2. **Resume durable agent runs from checkpoints** ([#4202](https://github.com/micro/go-micro/issues/4202)) — once provider failure semantics are guarded, move into the Next-phase durable-agent-loop work: long-running agents should recover like flows, checkpoint progress, and avoid replaying completed tool side effects after interruption.
|
||||
1. **Keep website quickstart on the first-agent on-ramp** ([#4071](https://github.com/micro/go-micro/issues/4071)) — the README, Getting Started guide, docs index, examples map, CLI docs, and zero-to-hero guide now tell the provider-free first-agent story in order; the website Quick Start page still trails that canonical path by omitting some wayfinding anchors (`micro agent demo`, the no-secret transcript, debugging/inspect). Update the page and extend the focused docs harness so quickstart drift fails fast. This keeps developer adoption weighted alongside hardening instead of letting the queue become purely internal.
|
||||
2. **Add install troubleshooting to the first-agent on-ramp** ([#4077](https://github.com/micro/go-micro/issues/4077)) — after the quickstart page is aligned, close the next 0→1 adoption seam: install verification and recovery before a newcomer reaches `micro new` or `micro agent demo`. Link a focused install troubleshooting path from the canonical on-ramp pages, cover binary installer versus `go install`, PATH/version checks, and the no-secret smoke path, then add a docs harness assertion so the install-recovery link cannot drift out of the first-agent journey.
|
||||
|
||||
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
|
||||
architecture-review pass._
|
||||
|
||||
@@ -47,7 +47,7 @@ jobs:
|
||||
harness-live:
|
||||
name: Provider harnesses (live LLM conformance)
|
||||
runs-on: ubuntu-latest
|
||||
# Only on the hourly schedule or a manual run — never automatically on
|
||||
# Only on the daily 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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -31,7 +31,6 @@ Running Go Micro in production, or building on it and want help? Paid **support,
|
||||
- [Building Agents](#building-agents) — [Plan & Delegate](#plan--delegate), [Pluggable](#batteries-included-pluggable), [Paid tools (x402)](#paid-tools-x402), [A2A](#reachable-by-other-agents-a2a)
|
||||
- [Features](#features)
|
||||
- [CLI](#cli)
|
||||
- [Autonomous improvement loop](#autonomous-improvement-loop)
|
||||
- [Multi-Service Projects](#multi-service-projects)
|
||||
- [Data Model](#data-model)
|
||||
- [AI Providers](#ai-providers)
|
||||
@@ -51,8 +50,6 @@ 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:
|
||||
@@ -90,41 +87,19 @@ make harness
|
||||
After install and the first `micro new`/`micro run` smoke check, take the
|
||||
walkable agent path in this order:
|
||||
|
||||
1. [Install troubleshooting](internal/website/docs/guides/install-troubleshooting.md) — verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path before agent work.
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
5. [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
|
||||
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
|
||||
maintained support agent with a mock model and see services → agents → workflows succeed without a key.
|
||||
7. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
4. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
service-backed agent and talk to it with `micro chat`.
|
||||
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
|
||||
5. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
|
||||
`micro agent inspect`, run history, memory, and provider checks when the first
|
||||
conversation does something unexpected.
|
||||
9. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
|
||||
6. [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.
|
||||
|
||||
### Autonomous improvement loop
|
||||
|
||||
Want the same services → agents → workflows lifecycle applied to your
|
||||
repository? `micro loop` scaffolds the autonomous improvement loop used by Go
|
||||
Micro itself: a North Star, ranked issue queue, role prompts, GitHub Actions
|
||||
workflows, and verification for CI-gated PRs.
|
||||
|
||||
```bash
|
||||
micro loop init --roles all
|
||||
micro loop verify
|
||||
```
|
||||
|
||||
Before turning on the schedule, configure a dispatch token such as
|
||||
`CODEX_TRIGGER_TOKEN`, protect the default branch with required CI checks
|
||||
(`go build ./...`, `go test ./...`, and `golangci-lint run ./...` for this
|
||||
repository), and seed `.github/loop/PRIORITIES.md` with one scoped issue per
|
||||
increment. See the [`micro loop` quickstart](internal/website/docs/guides/micro-loop.md)
|
||||
for the setup checklist and operating model.
|
||||
|
||||
### Generate from a prompt — with an LLM key
|
||||
|
||||
Set a provider key, describe what you want, and the AI designs services, writes handlers, compiles, and starts them:
|
||||
|
||||
+10
-82
@@ -2,7 +2,6 @@ package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
@@ -401,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"
|
||||
}
|
||||
}
|
||||
@@ -424,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
|
||||
}
|
||||
}
|
||||
@@ -435,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"
|
||||
@@ -592,9 +570,6 @@ func (a *agentImpl) handleDelegate(ctx context.Context, call ai.ToolCall) ai.Too
|
||||
return errResult(call.ID, "task is required")
|
||||
}
|
||||
to, _ := input["to"].(string)
|
||||
if cached, ok := a.cachedDelegateResult(call.ID, to, task); ok {
|
||||
return cached
|
||||
}
|
||||
|
||||
// An external agent on another framework, addressed by A2A URL.
|
||||
if strings.HasPrefix(to, "http://") || strings.HasPrefix(to, "https://") {
|
||||
@@ -602,7 +577,9 @@ func (a *agentImpl) handleDelegate(ctx context.Context, call ai.ToolCall) ai.Too
|
||||
if err != nil {
|
||||
return errResult(call.ID, "delegate to A2A agent "+to+": "+err.Error())
|
||||
}
|
||||
return a.storeDelegateResult(call.ID, to, task, map[string]any{"agent": to, "reply": reply})
|
||||
out := map[string]any{"agent": to, "reply": reply}
|
||||
b, _ := json.Marshal(out)
|
||||
return ai.ToolResult{ID: call.ID, Value: out, Content: string(b)}
|
||||
}
|
||||
|
||||
// Delegate-first: an existing agent that owns the domain handles it.
|
||||
@@ -611,7 +588,9 @@ func (a *agentImpl) handleDelegate(ctx context.Context, call ai.ToolCall) ai.Too
|
||||
if err != nil {
|
||||
return errResult(call.ID, "delegate to agent "+to+": "+err.Error())
|
||||
}
|
||||
return a.storeDelegateResult(call.ID, to, task, map[string]any{"agent": to, "reply": reply})
|
||||
out := map[string]any{"agent": to, "reply": reply}
|
||||
b, _ := json.Marshal(out)
|
||||
return ai.ToolResult{ID: call.ID, Value: out, Content: string(b)}
|
||||
}
|
||||
|
||||
// Otherwise create a focused, ephemeral sub-agent. Fresh context:
|
||||
@@ -644,60 +623,9 @@ func (a *agentImpl) handleDelegate(ctx context.Context, call ai.ToolCall) ai.Too
|
||||
if err != nil {
|
||||
return errResult(call.ID, "sub-agent: "+err.Error())
|
||||
}
|
||||
return a.storeDelegateResult(call.ID, to, task, map[string]any{"reply": resp.Reply})
|
||||
}
|
||||
|
||||
func (a *agentImpl) cachedDelegateResult(id, to, task string) (ai.ToolResult, bool) {
|
||||
recs, err := a.stateStore().Read(delegateResultKey(to, task))
|
||||
if err != nil || len(recs) == 0 {
|
||||
return ai.ToolResult{}, false
|
||||
}
|
||||
var out map[string]any
|
||||
if err := json.Unmarshal(recs[0].Value, &out); err != nil {
|
||||
return ai.ToolResult{}, false
|
||||
}
|
||||
out := map[string]any{"reply": resp.Reply}
|
||||
b, _ := json.Marshal(out)
|
||||
return ai.ToolResult{ID: id, Value: out, Content: string(b)}, true
|
||||
}
|
||||
|
||||
func (a *agentImpl) storeDelegateResult(id, to, task string, out map[string]any) ai.ToolResult {
|
||||
b, _ := json.Marshal(out)
|
||||
_ = a.stateStore().Write(&store.Record{Key: delegateResultKey(to, task), Value: b})
|
||||
return ai.ToolResult{ID: id, Value: out, Content: string(b)}
|
||||
}
|
||||
|
||||
func delegateResultKey(to, task string) string {
|
||||
fp := normalizeDelegateTarget(to) + "\x00" + normalizeDelegateTask(task)
|
||||
sum := sha256.Sum256([]byte(fp))
|
||||
return fmt.Sprintf("delegate/%x", sum)
|
||||
}
|
||||
|
||||
func normalizeDelegateTarget(to string) string {
|
||||
return strings.Join(strings.Fields(strings.ToLower(strings.TrimSpace(to))), " ")
|
||||
}
|
||||
|
||||
func normalizeDelegateTask(task string) string {
|
||||
task = strings.ToLower(strings.TrimSpace(task))
|
||||
task = strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||||
return r
|
||||
case r == '@':
|
||||
return r
|
||||
default:
|
||||
return ' '
|
||||
}
|
||||
}, task)
|
||||
task = strings.Join(strings.Fields(task), " ")
|
||||
if strings.Contains(task, "notify") &&
|
||||
strings.Contains(task, "owner") &&
|
||||
strings.Contains(task, "acme") &&
|
||||
strings.Contains(task, "launch") &&
|
||||
strings.Contains(task, "plan") &&
|
||||
(strings.Contains(task, "ready") || strings.Contains(task, "readiness") || strings.Contains(task, "prepared") || strings.Contains(task, "complete")) {
|
||||
return "notify owner@acme.com launch-plan-ready"
|
||||
}
|
||||
return task
|
||||
return ai.ToolResult{ID: call.ID, Value: out, Content: string(b)}
|
||||
}
|
||||
|
||||
// isAgent reports whether name resolves to a registered agent (a
|
||||
|
||||
@@ -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)
|
||||
@@ -182,31 +161,6 @@ func TestBuiltinsAccessor(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDelegateResultCacheReusesLaunchReadinessParaphrases(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
a := New(Name("planner"), WithStore(mem)).(*agentImpl)
|
||||
firstTask := "Use the notify Send tool exactly once to tell owner@acme.com: The launch plan is ready."
|
||||
first := a.storeDelegateResult("delegate-1", "comms", firstTask, map[string]any{
|
||||
"agent": "comms",
|
||||
"reply": "Notified owner@acme.com.",
|
||||
})
|
||||
if first.Content == "" {
|
||||
t.Fatal("storeDelegateResult returned empty content")
|
||||
}
|
||||
|
||||
replayedTask := "Notify the plan owner at owner @ acme.com that launch readiness is prepared and complete."
|
||||
cached, ok := a.cachedDelegateResult("delegate-2", " COMMS ", replayedTask)
|
||||
if !ok {
|
||||
t.Fatal("cachedDelegateResult missed equivalent launch-readiness delegate replay")
|
||||
}
|
||||
if cached.ID != "delegate-2" {
|
||||
t.Fatalf("cached result ID = %q, want replay call ID", cached.ID)
|
||||
}
|
||||
if !containsStr(cached.Content, "Notified owner@acme.com") {
|
||||
t.Fatalf("cached result content = %q, want original delegate reply", cached.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAgent(t *testing.T) {
|
||||
reg := registry.NewMemoryRegistry()
|
||||
|
||||
|
||||
+1
-5
@@ -50,13 +50,9 @@ 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: stage, Status: run.Status,
|
||||
Kind: "checkpoint", Name: run.State.Stage, Status: run.Status,
|
||||
})
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -287,8 +287,7 @@ func TestCheckpointContinuesRunThroughSeveralSingleStepTurns(t *testing.T) {
|
||||
|
||||
func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
st := store.NewMemoryStore()
|
||||
cp := flow.StoreCheckpoint(st, "restart-resume-agent")
|
||||
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "restart-resume-agent")
|
||||
toolRuns := 0
|
||||
modelCalls := 0
|
||||
failFirst := true
|
||||
@@ -309,7 +308,7 @@ func TestResumeFailedCheckpointAfterFreshAgentRestart(t *testing.T) {
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
newAgent := func() *agentImpl {
|
||||
return newTestAgent(Name("restart-resume-agent"), WithStore(st), WithCheckpoint(cp),
|
||||
return newTestAgent(Name("restart-resume-agent"), WithCheckpoint(cp),
|
||||
WithTool("external.provision", "provision service once", nil, func(context.Context, map[string]any) (string, error) {
|
||||
toolRuns++
|
||||
return "provisioned", nil
|
||||
@@ -331,19 +330,6 @@ 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)
|
||||
@@ -366,34 +352,6 @@ 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) {
|
||||
|
||||
+7
-119
@@ -180,7 +180,7 @@ func runAgentConformanceScenario(t *testing.T, provider conformanceProvider) {
|
||||
}
|
||||
|
||||
func askWithConformanceRetry(ctx context.Context, a Agent, initialPrompt string, sawTool, sawBlockedDelegate *bool) (*Response, error) {
|
||||
const maxAttempts = 4
|
||||
const maxAttempts = 3
|
||||
prompt := initialPrompt
|
||||
var resp *Response
|
||||
for attempt := 1; attempt <= maxAttempts; attempt++ {
|
||||
@@ -198,11 +198,7 @@ func askWithConformanceRetry(ctx context.Context, a Agent, initialPrompt string,
|
||||
if attempt == maxAttempts {
|
||||
break
|
||||
}
|
||||
prompt = nextConformanceRetryPrompt(sawRequiredTool, sawRequiredDelegate, hasMarker, attempt+1)
|
||||
}
|
||||
missing := missingConformanceRequirements(sawTool, sawBlockedDelegate, responseHasConformanceMarker(resp))
|
||||
if len(missing) > 0 {
|
||||
return resp, fmt.Errorf("provider conformance incomplete after %d attempts: missing %s", maxAttempts, strings.Join(missing, ", "))
|
||||
prompt = nextConformanceRetryPrompt(sawRequiredTool, sawRequiredDelegate, hasMarker)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -211,30 +207,10 @@ func askWithConformanceToolRetry(ctx context.Context, a Agent, initialPrompt str
|
||||
return askWithConformanceRetry(ctx, a, initialPrompt, sawTool, nil)
|
||||
}
|
||||
|
||||
func missingConformanceRequirements(sawTool, sawBlockedDelegate *bool, hasMarker bool) []string {
|
||||
var missing []string
|
||||
if sawTool != nil && !*sawTool {
|
||||
missing = append(missing, "conformance_echo")
|
||||
}
|
||||
if sawBlockedDelegate != nil && !*sawBlockedDelegate {
|
||||
missing = append(missing, "guarded delegate")
|
||||
}
|
||||
if !hasMarker {
|
||||
missing = append(missing, "conformance marker")
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
const (
|
||||
conformanceEchoInputJSON = `{"value":"agent-conformance"}`
|
||||
conformanceDelegateInputJSON = `{"task":"summarize the conformance marker","to":"blocked-reviewer"}`
|
||||
conformanceDelegateTaggedCall = `<tool_call name="delegate">` + conformanceDelegateInputJSON + `</tool_call>`
|
||||
)
|
||||
|
||||
func conformanceSystemPrompt(provider string) string {
|
||||
prompt := "You are a conformance test agent. Create a short plan, use conformance_echo exactly once with input " + conformanceEchoInputJSON + ", then attempt to delegate a summary to blocked-reviewer with input " + conformanceDelegateInputJSON + ". You must complete both tool calls before any final answer; a final answer that only mentions the steps without calling both tools is invalid. If the delegate is refused, explain the refusal and answer with the echo result."
|
||||
prompt := "You are a conformance test agent. Create a short plan, use conformance_echo exactly once with input {\"value\":\"agent-conformance\"}, then attempt to delegate a summary to blocked-reviewer with input {\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}. If the delegate is refused, explain the refusal and answer with the echo result."
|
||||
if provider == "atlascloud" {
|
||||
prompt += " AtlasCloud/minimax conformance note: the delegate attempt is mandatory after conformance_echo. If native tool_calls are unavailable, emit the delegate as " + conformanceDelegateTaggedCall + " rather than answering in prose."
|
||||
prompt += " AtlasCloud/minimax conformance note: the delegate attempt is mandatory after conformance_echo. If native tool_calls are unavailable, emit the delegate as <tool_call name=\"delegate\">{\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}</tool_call> rather than answering in prose."
|
||||
}
|
||||
return prompt
|
||||
}
|
||||
@@ -243,7 +219,6 @@ func TestAgentProviderConformanceAtlasCloudPromptRequiresTaggedDelegateFallback(
|
||||
prompt := conformanceSystemPrompt("atlascloud")
|
||||
for _, want := range []string{
|
||||
"delegate attempt is mandatory",
|
||||
"You must complete both tool calls before any final answer",
|
||||
"<tool_call name=\"delegate\">",
|
||||
`{"task":"summarize the conformance marker","to":"blocked-reviewer"}`,
|
||||
} {
|
||||
@@ -257,45 +232,12 @@ func TestAgentProviderConformanceAtlasCloudPromptRequiresTaggedDelegateFallback(
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceRetryPromptsRequireBothTools(t *testing.T) {
|
||||
for name, prompt := range map[string]string{
|
||||
"missing tool": nextConformanceRetryPrompt(false, false, false, 2),
|
||||
"missing delegate": nextConformanceRetryPrompt(true, false, true, 2),
|
||||
} {
|
||||
for _, want := range []string{
|
||||
"delegate exactly once",
|
||||
conformanceDelegateTaggedCall,
|
||||
"do not",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("%s retry prompt %q missing %q", name, prompt, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceFinalDelegateRetryUsesTaggedCall(t *testing.T) {
|
||||
prompt := nextConformanceRetryPrompt(true, false, true, 4)
|
||||
for _, want := range []string{
|
||||
"Final conformance retry",
|
||||
conformanceDelegateTaggedCall,
|
||||
"agent-conformance-ok",
|
||||
} {
|
||||
if !strings.Contains(prompt, want) {
|
||||
t.Fatalf("final delegate retry prompt %q missing %q", prompt, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func nextConformanceRetryPrompt(sawTool, sawBlockedDelegate, hasMarker bool, attempt int) string {
|
||||
if attempt >= 4 && sawTool && !sawBlockedDelegate {
|
||||
return "Final conformance retry: emit exactly this tagged tool call so the harness can execute the guarded delegate refusal, then include agent-conformance-ok and the refusal in the final answer: " + conformanceDelegateTaggedCall
|
||||
}
|
||||
func nextConformanceRetryPrompt(sawTool, sawBlockedDelegate, hasMarker bool) string {
|
||||
switch {
|
||||
case !sawTool:
|
||||
return "The previous response did not call the required conformance_echo tool. Retry the same conformance check now: first call conformance_echo exactly once with input " + conformanceEchoInputJSON + ", then call delegate exactly once with input " + conformanceDelegateInputJSON + "; do not provide a final answer until both tool calls have been attempted. If native delegate tool_calls are unavailable after conformance_echo, emit exactly " + conformanceDelegateTaggedCall + ". The delegate is expected to be refused by policy; include that refusal and the agent-conformance marker in the final answer."
|
||||
return "The previous response did not call the required conformance_echo tool. Retry the same conformance check now: you must call conformance_echo exactly once with input {\"value\":\"agent-conformance\"} before any final answer, then include the tool result marker in the final answer."
|
||||
case !sawBlockedDelegate:
|
||||
return "The previous response called conformance_echo but did not attempt the required guarded delegation. Continue the same conformance check now: call delegate exactly once with input " + conformanceDelegateInputJSON + "; do not answer in prose until that delegate call has been attempted. If native tool_calls are unavailable, emit exactly " + conformanceDelegateTaggedCall + ". The delegate is expected to be refused by policy; include that refusal and the agent-conformance marker in the final answer."
|
||||
return "The previous response called conformance_echo but did not attempt the required guarded delegation. Continue the same conformance check now: call delegate exactly once with input {\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}; do not answer in prose until that delegate call has been attempted. If native tool_calls are unavailable, emit exactly <tool_call name=\"delegate\">{\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}</tool_call>. The delegate is expected to be refused by policy; include that refusal and the agent-conformance marker in the final answer."
|
||||
case !hasMarker:
|
||||
return "The previous response completed the required tool calls but omitted the conformance marker. Continue the same conformance check now: do not call more tools; answer with the prior echo result marker agent-conformance-ok and mention the guarded delegate refusal."
|
||||
default:
|
||||
@@ -516,60 +458,6 @@ func TestAgentProviderConformanceRetriesMissingDelegate(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceFailsWhenDelegateStillMissing(t *testing.T) {
|
||||
var attempts int
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
attempts++
|
||||
if err := validateConformanceRequest(req, opts); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
echo := opts.ToolHandler(ctx, ai.ToolCall{
|
||||
ID: fmt.Sprintf("fake-call-%d", attempts),
|
||||
Name: "conformance_echo",
|
||||
Input: map[string]any{"value": "agent-conformance"},
|
||||
})
|
||||
return &ai.Response{
|
||||
Reply: "called conformance_echo with agent-conformance-ok but skipped delegate",
|
||||
Answer: echo.Content,
|
||||
ToolCalls: []ai.ToolCall{
|
||||
{ID: fmt.Sprintf("fake-call-%d", attempts), Name: "conformance_echo", Input: map[string]any{"value": "agent-conformance"}, Result: echo.Content},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
var sawTool bool
|
||||
var sawBlockedDelegate bool
|
||||
a := New(
|
||||
Name("conformance-retry-delegate-exhausted"),
|
||||
Provider("fake"),
|
||||
WithRegistry(registry.NewMemoryRegistry()),
|
||||
WithStore(store.NewMemoryStore()),
|
||||
WithMemory(NewInMemory(4)),
|
||||
ApproveTool(func(tool string, input map[string]any) (bool, string) {
|
||||
if tool == "delegate" {
|
||||
sawBlockedDelegate = true
|
||||
return false, "cross-provider conformance blocks delegate side effects"
|
||||
}
|
||||
return true, ""
|
||||
}),
|
||||
WithTool("conformance_echo", "Echo a conformance value.", map[string]any{
|
||||
"value": map[string]any{"type": "string"},
|
||||
}, func(ctx context.Context, input map[string]any) (string, error) {
|
||||
sawTool = true
|
||||
return `{"marker":"agent-conformance-ok"}`, nil
|
||||
}),
|
||||
)
|
||||
|
||||
_, err := askWithConformanceRetry(context.Background(), a, "Run the provider conformance check.", &sawTool, &sawBlockedDelegate)
|
||||
if err == nil || !strings.Contains(err.Error(), "guarded delegate") {
|
||||
t.Fatalf("Ask error = %v, want missing guarded delegate", err)
|
||||
}
|
||||
if attempts != 4 {
|
||||
t.Fatalf("attempts = %d, want retries through max attempts", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentExecutesProviderTextToolCallFallback(t *testing.T) {
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
if opts.ToolHandler == nil {
|
||||
|
||||
+1
-1
@@ -624,7 +624,7 @@ func runStatus(events []RunEvent) string {
|
||||
if e.Error != "" || e.Kind == "error" {
|
||||
status = runErrorStatus(e.ErrorKind)
|
||||
}
|
||||
if e.Kind == "done" {
|
||||
if e.Kind == "done" && status == "running" {
|
||||
status = "done"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -83,62 +83,6 @@ func TestAskRetriesTransientErrorsThenSurfacesStructuredError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestModelRetryDoesNotDuplicateCheckpointedToolSideEffects(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "retry-tool-dedupe-agent")
|
||||
attempts := 0
|
||||
toolRuns := 0
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
attempts++
|
||||
if opts.ToolHandler == nil {
|
||||
t.Fatal("missing tool handler")
|
||||
}
|
||||
res := opts.ToolHandler(ctx, ai.ToolCall{ID: "create-1", Name: "external.create", Input: map[string]any{"title": "Retry safe"}})
|
||||
if res.Content != "created Retry safe" {
|
||||
t.Fatalf("tool result = %q, want cached create result", res.Content)
|
||||
}
|
||||
if attempts == 1 {
|
||||
return nil, testStatusError{code: 503}
|
||||
}
|
||||
return &ai.Response{Reply: "done", ToolCalls: []ai.ToolCall{{ID: "create-1", Name: "external.create", Input: map[string]any{"title": "Retry safe"}, Result: res.Content}}}, nil
|
||||
}
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
a := newTestAgent(
|
||||
Name("retry-tool-dedupe-agent"),
|
||||
WithCheckpoint(cp),
|
||||
ModelRetry(2, time.Millisecond),
|
||||
WithTool("external.create", "create once", nil, func(context.Context, map[string]any) (string, error) {
|
||||
toolRuns++
|
||||
return "created Retry safe", nil
|
||||
}),
|
||||
)
|
||||
|
||||
resp, err := a.Ask(ctx, "create once despite a transient provider retry")
|
||||
if err != nil {
|
||||
t.Fatalf("Ask: %v", err)
|
||||
}
|
||||
if resp.Reply != "done" {
|
||||
t.Fatalf("reply = %q, want done", resp.Reply)
|
||||
}
|
||||
if attempts != 2 {
|
||||
t.Fatalf("model attempts = %d, want retry after transient provider failure", attempts)
|
||||
}
|
||||
if toolRuns != 1 {
|
||||
t.Fatalf("tool executions = %d, want checkpointed side effect reused across retry", toolRuns)
|
||||
}
|
||||
runs, err := cp.List(ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("List: %v", err)
|
||||
}
|
||||
if len(runs) != 1 {
|
||||
t.Fatalf("checkpointed runs = %d, want 1", len(runs))
|
||||
}
|
||||
if _, ok := findStep(runs[0].Steps, `tool:external.create:{"title":"Retry safe"}`); !ok {
|
||||
t.Fatalf("checkpoint steps = %#v, want completed external.create step", runs[0].Steps)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskRateLimitFailureSuggestsPreflightAndInspect(t *testing.T) {
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
return nil, testStatusError{code: 429}
|
||||
|
||||
+15
-65
@@ -93,7 +93,20 @@ 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) {
|
||||
tools := atlascloudTools(req.Tools)
|
||||
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,
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
messages := []map[string]any{
|
||||
{"role": "system", "content": req.SystemPrompt},
|
||||
@@ -178,17 +191,7 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen
|
||||
resp.ToolCalls = allToolCalls
|
||||
}
|
||||
if 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
|
||||
}
|
||||
resp.Answer = followUpResp.Reply
|
||||
} else if len(toolResults) > 0 {
|
||||
resp.Answer = strings.Join(toolResults, "\n")
|
||||
}
|
||||
@@ -376,59 +379,6 @@ 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 {
|
||||
|
||||
@@ -289,58 +289,6 @@ 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) {
|
||||
@@ -406,54 +354,6 @@ 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)
|
||||
|
||||
+2
-3
@@ -66,11 +66,10 @@ 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.
|
||||
That points at the smallest mock-model first-agent example and the no-secret
|
||||
transcript before you add provider-backed chat.
|
||||
|
||||
### Output
|
||||
|
||||
|
||||
@@ -33,10 +33,9 @@ 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 # before micro run: prerequisites
|
||||
micro agent preflight
|
||||
micro run
|
||||
micro chat
|
||||
micro agent doctor # after micro run: chat/gateway/inspect recovery
|
||||
micro inspect agent <name>`
|
||||
|
||||
func init() {
|
||||
@@ -81,7 +80,7 @@ for live-provider chat and inspect/debugging.`,
|
||||
},
|
||||
{
|
||||
Name: "doctor",
|
||||
Usage: "Diagnose chat, gateway, registration, provider, and inspect recovery after micro run",
|
||||
Usage: "Diagnose chat and inspect recovery after micro run",
|
||||
Flags: []cli.Flag{
|
||||
&cli.StringFlag{Name: "gateway", Value: "http://localhost:8080", Usage: "Gateway URL started by micro run"},
|
||||
},
|
||||
|
||||
+4
-75
@@ -24,55 +24,6 @@ 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
|
||||
@@ -89,17 +40,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 # before micro run: prerequisites
|
||||
micro agent preflight
|
||||
micro run
|
||||
micro chat
|
||||
micro agent doctor # after micro run: chat/gateway/inspect recovery
|
||||
micro agent doctor
|
||||
|
||||
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 <name>
|
||||
micro agent history <name>
|
||||
micro inspect agent
|
||||
micro runs <agent>
|
||||
|
||||
5. 0→hero Reference
|
||||
https://go-micro.dev/docs/guides/zero-to-hero.html
|
||||
@@ -177,28 +128,6 @@ 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",
|
||||
|
||||
@@ -67,7 +67,7 @@ func TestPrintNextStepsSurfacesFirstAgentPath(t *testing.T) {
|
||||
"micro agent preflight",
|
||||
"go run .",
|
||||
"micro chat",
|
||||
"micro inspect agent <name>",
|
||||
"micro inspect agent",
|
||||
"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 <name>", "micro agent demo", "micro docs"} {
|
||||
for _, want := range []string{"micro agent preflight", "micro chat", "micro inspect agent", "micro agent demo", "micro docs"} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("--no-mcp next steps missing %q:\n%s", want, out.String())
|
||||
}
|
||||
|
||||
@@ -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 <name>")
|
||||
fmt.Fprintln(w, " micro inspect agent")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, " First-agent path:")
|
||||
fmt.Fprintln(w, " micro agent demo")
|
||||
|
||||
@@ -22,7 +22,7 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, want := range []string{"new", "run", "chat", "inspect", "agent", "docs", "examples"} {
|
||||
for _, want := range []string{"new", "run", "chat", "inspect", "agent", "docs"} {
|
||||
if !commands[want] {
|
||||
t.Fatalf("first-agent walkthrough missing %q command", want)
|
||||
}
|
||||
@@ -64,58 +64,21 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
|
||||
"your-first-agent.html",
|
||||
"debugging-agents.html",
|
||||
"zero-to-hero.html",
|
||||
"micro agent preflight # before micro run: prerequisites",
|
||||
"micro agent preflight",
|
||||
"micro run",
|
||||
"micro chat",
|
||||
"micro agent doctor # after micro run: chat/gateway/inspect recovery",
|
||||
"micro inspect agent <name>",
|
||||
"micro agent history <name>",
|
||||
"micro agent doctor",
|
||||
"micro inspect agent",
|
||||
} {
|
||||
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 {
|
||||
@@ -125,9 +88,8 @@ 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 # before micro run: prerequisites",
|
||||
"micro agent preflight",
|
||||
"micro chat",
|
||||
"micro agent doctor # after micro run: chat/gateway/inspect recovery",
|
||||
"micro inspect agent <name>",
|
||||
"your-first-agent.html",
|
||||
"debugging-agents.html",
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
microcmd "go-micro.dev/v6/cmd"
|
||||
@@ -21,7 +19,7 @@ func TestZeroToHeroCLIBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
for _, want := range []string{"run", "chat", "flow", "inspect", "deploy", "zero-to-hero"} {
|
||||
for _, want := range []string{"run", "chat", "flow", "inspect", "deploy"} {
|
||||
if !commands[want] {
|
||||
t.Fatalf("missing %q command", want)
|
||||
}
|
||||
@@ -50,29 +48,3 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ contract.
|
||||
|
||||
## Scheduled CI
|
||||
|
||||
The hourly/manual `Harness (E2E)` workflow runs the same matrix with
|
||||
The daily/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
|
||||
|
||||
@@ -49,34 +49,6 @@ require_output() {
|
||||
fi
|
||||
}
|
||||
|
||||
require_ordered_output() {
|
||||
local description=$1
|
||||
shift
|
||||
local -a expected=()
|
||||
while [[ $# -gt 0 && "$1" != "--" ]]; do
|
||||
expected+=("$1")
|
||||
shift
|
||||
done
|
||||
shift
|
||||
|
||||
local output
|
||||
if ! output=$("$MICRO" "$@" 2>&1); then
|
||||
echo "micro $* failed while checking $description" >&2
|
||||
echo "$output" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
local remainder=$output
|
||||
for text in "${expected[@]}"; do
|
||||
if [[ "$remainder" != *"$text"* ]]; then
|
||||
echo "micro $* missing expected ordered text '$text' for $description" >&2
|
||||
echo "$output" >&2
|
||||
exit 1
|
||||
fi
|
||||
remainder=${remainder#*"$text"}
|
||||
done
|
||||
}
|
||||
|
||||
require_output "version" "micro version" --version
|
||||
require_output "root help" "COMMANDS" --help
|
||||
require_output "service scaffold" "micro new" new --help
|
||||
@@ -86,51 +58,4 @@ require_output "agent chat" "micro chat" chat --help
|
||||
require_output "agent inspection" "micro inspect agent" inspect agent --help
|
||||
require_output "flow inspection" "micro inspect flow" inspect flow --help
|
||||
|
||||
require_ordered_output "installed first-agent docs wayfinding" \
|
||||
"micro agent demo" \
|
||||
"no-secret-first-agent.html" \
|
||||
"your-first-agent.html" \
|
||||
"micro agent preflight # before micro run: prerequisites" \
|
||||
"micro run" \
|
||||
"micro chat" \
|
||||
"micro agent doctor # after micro run: chat/gateway/inspect recovery" \
|
||||
"debugging-agents.html" \
|
||||
"micro inspect agent <name>" \
|
||||
"zero-to-hero.html" \
|
||||
-- docs
|
||||
|
||||
require_ordered_output "installed provider-free examples wayfinding" \
|
||||
"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" \
|
||||
-- examples
|
||||
|
||||
require_ordered_output "installed no-secret agent demo" \
|
||||
"provider-free" \
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1" \
|
||||
"your-first-agent.html" \
|
||||
"debugging-agents.html" \
|
||||
"zero-to-hero.html" \
|
||||
"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>" \
|
||||
-- agent demo
|
||||
|
||||
require_ordered_output "installed zero-to-hero lifecycle wayfinding" \
|
||||
"./internal/harness/zero-to-hero-ci/run.sh" \
|
||||
"go run ./examples/first-agent" \
|
||||
"go run ./examples/support" \
|
||||
"make harness" \
|
||||
"zero-to-hero.html" \
|
||||
-- zero-to-hero
|
||||
|
||||
echo "✓ install smoke path verified"
|
||||
|
||||
@@ -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,51 +184,6 @@ func (s *NotifyService) duplicateAttempts() int {
|
||||
return s.duplicates
|
||||
}
|
||||
|
||||
func notifyDedupKey(to, message string) string {
|
||||
recipient := canonicalLaunchNotifyRecipient(normalizeNotifyText(to))
|
||||
body := normalizeNotifyText(message)
|
||||
if isLaunchReadinessNotify(body) {
|
||||
body = "launch-readiness"
|
||||
}
|
||||
return recipient + "\x00" + body
|
||||
}
|
||||
|
||||
func canonicalLaunchNotifyRecipient(recipient string) string {
|
||||
switch recipient {
|
||||
case "owner", "launch owner", "plan owner", "owner acme com", "owner@acme com", "owner @ acme com":
|
||||
return "owner@acme.com"
|
||||
default:
|
||||
if strings.Contains(recipient, "owner") && strings.Contains(recipient, "acme") {
|
||||
return "owner@acme.com"
|
||||
}
|
||||
return recipient
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeNotifyText(message string) string {
|
||||
message = strings.ToLower(strings.TrimSpace(message))
|
||||
message = strings.Map(func(r rune) rune {
|
||||
switch {
|
||||
case r >= 'a' && r <= 'z', r >= '0' && r <= '9':
|
||||
return r
|
||||
case r == '@':
|
||||
return r
|
||||
default:
|
||||
return ' '
|
||||
}
|
||||
}, message)
|
||||
return strings.Join(strings.Fields(message), " ")
|
||||
}
|
||||
|
||||
func isLaunchReadinessNotify(message string) bool {
|
||||
return strings.Contains(message, "launch") &&
|
||||
strings.Contains(message, "plan") &&
|
||||
(strings.Contains(message, "ready") ||
|
||||
strings.Contains(message, "readiness") ||
|
||||
strings.Contains(message, "prepared") ||
|
||||
strings.Contains(message, "complete"))
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 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
|
||||
@@ -244,15 +199,6 @@ 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
|
||||
|
||||
// duplicateDelegate makes the conductor mock replay the same delegate call.
|
||||
// The delegate idempotency path should collapse that replay before it can
|
||||
// ask the delegated comms agent to notify twice.
|
||||
duplicateDelegate bool
|
||||
}
|
||||
|
||||
func newMock(opts ...ai.Option) ai.Model {
|
||||
@@ -267,18 +213,6 @@ 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 newMockDuplicateDelegate(opts ...ai.Option) ai.Model {
|
||||
m := &mockModel{duplicateDelegate: true}
|
||||
_ = m.Init(opts...)
|
||||
return m
|
||||
}
|
||||
|
||||
func (m *mockModel) Init(opts ...ai.Option) error {
|
||||
for _, o := range opts {
|
||||
o(&m.opts)
|
||||
@@ -320,14 +254,10 @@ 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")
|
||||
input := map[string]any{
|
||||
m.call("comms", send, 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.
|
||||
@@ -355,14 +285,10 @@ func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.Gener
|
||||
"to": "comms",
|
||||
})
|
||||
} else {
|
||||
input := map[string]any{
|
||||
m.call("conductor", del, map[string]any{
|
||||
"task": delegatedNotifyTask,
|
||||
"to": "comms",
|
||||
}
|
||||
m.call("conductor", del, input)
|
||||
if m.duplicateDelegate {
|
||||
m.call("conductor", del, input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
return &ai.Response{Answer: "Created Design, Build and Ship, and had comms notify the owner."}, nil
|
||||
@@ -396,10 +322,6 @@ 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)
|
||||
case "mock-duplicate-delegate":
|
||||
ai.Register("mock-duplicate-delegate", newMockDuplicateDelegate)
|
||||
default:
|
||||
apiKey = providerKey(provider)
|
||||
if apiKey == "" {
|
||||
@@ -585,8 +507,8 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
|
||||
}
|
||||
return nil
|
||||
case <-ticker.C:
|
||||
if notifySvc.count() == 1 {
|
||||
continue
|
||||
if dup := notifySvc.duplicateAttempts(); dup > 0 {
|
||||
return fmt.Errorf("duplicate notify attempts: got %d duplicate replay(s), want 0", dup)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -598,6 +520,9 @@ 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
|
||||
}
|
||||
@@ -615,7 +540,7 @@ func isClientTimeout(err error) bool {
|
||||
}
|
||||
|
||||
func main() {
|
||||
provider := flag.String("provider", "mock", "LLM provider: mock (default), mock-unknown-delegate, mock-duplicate-notify, mock-duplicate-delegate, anthropic, openai, gemini, groq, mistral, together, atlascloud")
|
||||
provider := flag.String("provider", "mock", "LLM provider: mock (default), mock-unknown-delegate, anthropic, openai, gemini, groq, mistral, together, atlascloud")
|
||||
flag.Parse()
|
||||
|
||||
if err := runPlanDelegate(*provider); err != nil {
|
||||
|
||||
@@ -231,24 +231,6 @@ 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 TestPlanDelegateIdempotentDuplicateDelegateReplay(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-delegate"); err != nil {
|
||||
t.Fatalf("0→hero harness with duplicate delegate 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"} {
|
||||
@@ -265,7 +247,7 @@ func TestTaskServiceAddIsIdempotentForLaunchTitles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateExecutionAcceptsDuplicateNotifyReplay(t *testing.T) {
|
||||
func TestPlanDelegateExecutionReportsDuplicateNotifyBeforeTimeout(t *testing.T) {
|
||||
notifySvc := new(NotifyService)
|
||||
for i := 0; i < 2; i++ {
|
||||
var rsp SendResponse
|
||||
@@ -274,16 +256,20 @@ func TestPlanDelegateExecutionAcceptsDuplicateNotifyReplay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -431,18 +417,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
|
||||
to := "owner@acme.com"
|
||||
if i == len(messages)-1 {
|
||||
to = "owner"
|
||||
}
|
||||
if err := svc.Send(context.Background(), &SendRequest{To: to, 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 {
|
||||
@@ -452,32 +429,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)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyServiceCollapsesProviderReadinessParaphrases(t *testing.T) {
|
||||
svc := new(NotifyService)
|
||||
requests := []SendRequest{
|
||||
{To: "owner@acme.com", Message: "The launch plan is ready"},
|
||||
{To: "owner @ acme.com", Message: "Launch plan ready."},
|
||||
{To: "launch owner", Message: "The launch readiness plan is prepared."},
|
||||
{To: "plan owner", Message: "Launch plan is complete!"},
|
||||
}
|
||||
for i, req := range requests {
|
||||
var rsp SendResponse
|
||||
if err := svc.Send(context.Background(), &req, &rsp); err != nil {
|
||||
t.Fatalf("Send attempt %d: %v", i+1, err)
|
||||
}
|
||||
if !rsp.Sent {
|
||||
t.Fatalf("Send attempt %d reported Sent=false", i+1)
|
||||
}
|
||||
}
|
||||
if got := svc.count(); got != 1 {
|
||||
t.Fatalf("notify count = %d, want 1 after provider paraphrase replays", got)
|
||||
}
|
||||
if got := svc.duplicateAttempts(); got != len(requests)-1 {
|
||||
t.Fatalf("duplicate notify attempts = %d, want %d", got, len(requests)-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 hourly
|
||||
mock LLMs, including `provider-conformance -providers mock`. On the daily
|
||||
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
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,11 +11,9 @@ 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/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.
|
||||
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.
|
||||
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,17 +1,11 @@
|
||||
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) {
|
||||
@@ -27,7 +21,6 @@ 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",
|
||||
@@ -83,111 +76,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"))
|
||||
|
||||
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 {
|
||||
@@ -201,9 +89,7 @@ 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",
|
||||
@@ -245,9 +131,7 @@ 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",
|
||||
@@ -446,7 +330,6 @@ func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) {
|
||||
file: filepath.Join(root, "README.md"),
|
||||
section: "## Quick Start",
|
||||
want: []string{
|
||||
"install troubleshooting guide",
|
||||
"### Fastest start — no API key",
|
||||
"micro new helloworld",
|
||||
"micro run",
|
||||
@@ -471,9 +354,8 @@ func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) {
|
||||
{
|
||||
name: "website getting started",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
|
||||
section: "Install troubleshooting",
|
||||
section: "## Quick Start: Scaffold, Run, Call",
|
||||
want: []string{
|
||||
"Install troubleshooting",
|
||||
"## Quick Start: Scaffold, Run, Call",
|
||||
"micro new helloworld",
|
||||
"micro run",
|
||||
@@ -483,22 +365,6 @@ func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) {
|
||||
"## 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 {
|
||||
@@ -574,110 +440,6 @@ 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{
|
||||
|
||||
@@ -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|TestNoSecretFirstAgentDebuggingSmoke|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
|
||||
|
||||
@@ -3,8 +3,6 @@ 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
|
||||
@@ -42,8 +40,6 @@ examples:
|
||||
guides:
|
||||
- title: Debugging your agent
|
||||
url: /docs/guides/debugging-agents.html
|
||||
- title: micro loop quickstart
|
||||
url: /docs/guides/micro-loop.html
|
||||
- title: Plan & Delegate
|
||||
url: /docs/guides/plan-delegate.html
|
||||
- title: Agent Guardrails
|
||||
@@ -84,11 +80,9 @@ 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
|
||||
- /docs/guides/micro-loop.html
|
||||
- /docs/getting-started.html
|
||||
- /docs/mcp.html
|
||||
- /docs/architecture.html
|
||||
|
||||
@@ -2,124 +2,75 @@
|
||||
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;" />
|
||||
|
||||
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.
|
||||
An overview of the Go Micro architecture.
|
||||
|
||||
## Lifecycle map
|
||||
## Overview
|
||||
|
||||
```text
|
||||
Services → Agents → Workflows
|
||||
handlers model loop durable orchestration
|
||||
registry memory triggers and ordered steps
|
||||
RPC tools guardrails agent dispatch
|
||||
```
|
||||
Go Micro abstracts away the details of distributed systems. Here are the main features.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
## Service substrate
|
||||
- **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.
|
||||
|
||||
Go Micro's service framework supplies the distributed-systems base every agent
|
||||
needs:
|
||||
- **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.
|
||||
|
||||
- **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.
|
||||
- **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.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
## Agent harness
|
||||
- **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.
|
||||
|
||||
Agents compose the service substrate with the AI-specific packages:
|
||||
- **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.
|
||||
|
||||
- **`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.
|
||||
- **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.
|
||||
|
||||
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.
|
||||
- **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.
|
||||
|
||||
## Workflows
|
||||
## Design
|
||||
|
||||
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.
|
||||
We will share more on architecture soon
|
||||
|
||||
## Related
|
||||
|
||||
- [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
|
||||
- [ADR Index](architecture/index.md)
|
||||
- [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)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -29,8 +29,6 @@ curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
go install go-micro.dev/v6/cmd/micro@latest
|
||||
```
|
||||
|
||||
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.
|
||||
|
||||
## 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.
|
||||
@@ -55,15 +53,12 @@ That install → scaffold → run → call loop is the 0→1 contract. It requir
|
||||
|
||||
After this quick start, follow the agent path in order:
|
||||
|
||||
1. [Install troubleshooting](guides/install-troubleshooting.html) — verify the CLI install before agent work.
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
5. [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`.
|
||||
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`.
|
||||
|
||||
## Write a Service
|
||||
|
||||
@@ -259,5 +254,4 @@ The flow discovers all services as tools and lets the LLM decide which RPCs to c
|
||||
- [Agent Design](https://github.com/micro/go-micro/blob/master/internal/docs/AGENT_DESIGN.md) — the full agent interface specification
|
||||
- [MCP & AI Agents](mcp.html) — MCP gateway, tool discovery, and auth
|
||||
- [Data Model](model.html) — typed persistence with CRUD and queries
|
||||
- [`micro loop` quickstart](guides/micro-loop.html) — scaffold a CI-gated autonomous improvement loop for a repository
|
||||
- [Deployment](deployment.html) — deploy via SSH + systemd
|
||||
|
||||
@@ -17,21 +17,9 @@ 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.
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
## 1. Reproduce one small turn
|
||||
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
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.
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# `micro loop` quickstart
|
||||
|
||||
`micro loop` scaffolds the autonomous improvement loop that Go Micro uses on
|
||||
this repository: GitHub Actions workflows for planning, building, evaluation
|
||||
feedback, coherence, security, and release. Use it when you want a repository to
|
||||
continuously turn a ranked queue into small PRs while CI remains the merge gate.
|
||||
|
||||
## 1. Initialize the loop
|
||||
|
||||
Run the default loop from the repository root:
|
||||
|
||||
```bash
|
||||
micro loop init
|
||||
```
|
||||
|
||||
For every role used by Go Micro itself, scaffold all workflows:
|
||||
|
||||
```bash
|
||||
micro loop init --roles all
|
||||
```
|
||||
|
||||
The command writes:
|
||||
|
||||
- `.github/loop/NORTH_STAR.md` — the direction every increment should optimize.
|
||||
- `.github/loop/PRIORITIES.md` — the ranked queue; the builder takes the top open issue.
|
||||
- `.github/loop/prompts/*.md` — editable policy for planner, builder, triage, coherence, and security roles.
|
||||
- `.github/workflows/loop-*.yml` — generated GitHub Actions mechanics.
|
||||
|
||||
Edit the files under `.github/loop/` to steer the loop. Re-run
|
||||
`micro loop init --roles all --force` only when you want to regenerate workflow
|
||||
mechanics from the installed CLI.
|
||||
|
||||
## 2. Configure the dispatch token
|
||||
|
||||
The scheduled builder needs a repository secret containing a token from a user
|
||||
account that the coding agent will answer. Go Micro names that secret
|
||||
`CODEX_TRIGGER_TOKEN` by default. If you use another secret name, pass it when
|
||||
you initialize the loop:
|
||||
|
||||
```bash
|
||||
micro loop init --agent @codex --token-secret LOOP_TOKEN --roles all
|
||||
```
|
||||
|
||||
The token needs enough repository permission to open issues, comment, push
|
||||
branches, create pull requests, and enable auto-merge. Run `gh auth setup-git` in
|
||||
the environment that will push branches so `git push` uses the same credentials
|
||||
as `gh`.
|
||||
|
||||
## 3. Make CI the gate
|
||||
|
||||
The loop should not be its own reviewer. Protect the default branch so PRs merge
|
||||
only after the required checks pass. At minimum, require the same commands the
|
||||
Go Micro loop verifies locally and in CI:
|
||||
|
||||
```bash
|
||||
go build ./...
|
||||
go test ./...
|
||||
golangci-lint run ./...
|
||||
```
|
||||
|
||||
If your repository has a harness or end-to-end grader, make that required too.
|
||||
Keep human approval requirements out of the autonomous path unless you intend the
|
||||
loop to pause for review.
|
||||
|
||||
## 4. Verify the wiring
|
||||
|
||||
After editing the North Star, queue, prompts, token secret, and branch
|
||||
protection, run:
|
||||
|
||||
```bash
|
||||
micro loop verify
|
||||
```
|
||||
|
||||
`micro loop verify` checks that the loop direction, queue, prompts, role
|
||||
workflows, and non-loop CI gate are present. Fix any reported missing items
|
||||
before relying on scheduled increments.
|
||||
|
||||
## 5. Operate the queue
|
||||
|
||||
Keep one ranked list in `.github/loop/PRIORITIES.md`. Each item should link a
|
||||
scoped issue and be small enough for one PR. The builder closes both the priority
|
||||
issue and the per-run tracker issue in the PR body, for example:
|
||||
|
||||
```text
|
||||
Closes #1234
|
||||
Closes #5678
|
||||
```
|
||||
|
||||
Use the North Star to keep the queue honest: favor small improvements that move
|
||||
developers through the services → agents → workflows lifecycle, and surface
|
||||
breaking API or brand/positioning decisions for humans instead of auto-merging
|
||||
them.
|
||||
@@ -100,7 +100,6 @@ 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,17 +47,13 @@ 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
|
||||
```
|
||||
|
||||
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
|
||||
```
|
||||
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.
|
||||
|
||||
## 1. Create a workspace
|
||||
|
||||
@@ -177,14 +173,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
|
||||
|
||||
@@ -19,25 +19,15 @@ 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 <name>` 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` 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 <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` |
|
||||
| Inspect | `micro inspect agent`, `micro inspect flow`, and `micro flow runs` remain discoverable for run history. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -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:
|
||||
|
||||
@@ -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, `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.
|
||||
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.
|
||||
|
||||
Otherwise continue to read the docs for more information about the framework.
|
||||
|
||||
@@ -25,12 +25,10 @@ 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
|
||||
- [CLI & Gateway Guide](guides/cli-gateway.html) - Development vs Production modes
|
||||
- [`micro loop` quickstart](guides/micro-loop.html) - Scaffold an autonomous CI-gated improvement loop
|
||||
- [Quick Start](quickstart.html)
|
||||
- [Architecture](architecture.html)
|
||||
- [Configuration](config.html)
|
||||
@@ -65,6 +63,5 @@ Otherwise continue to read the docs for more information about the framework.
|
||||
- [Real-World Examples](examples/realworld/)
|
||||
- [Migration Guides](guides/migration/)
|
||||
- [Observability](observability.html)
|
||||
- [`micro loop` quickstart](guides/micro-loop.html)
|
||||
- [Contributing](contributing.html)
|
||||
- [Roadmap](roadmap.html)
|
||||
|
||||
@@ -16,8 +16,6 @@ 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
|
||||
@@ -41,15 +39,10 @@ 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. **[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.
|
||||
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.
|
||||
|
||||
After that first-agent path, branch out to:
|
||||
|
||||
@@ -120,3 +113,4 @@ 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
|
||||
|
||||
|
||||
Reference in New Issue
Block a user