Compare commits
36 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| be49540d42 | |||
| 28c411f0f7 | |||
| 99a956dec3 | |||
| 84cb4532f5 | |||
| 3d0ea0666e | |||
| cddf85c218 | |||
| c4eec47cbc | |||
| 87f011471b | |||
| 8ed0c21aa3 | |||
| 93ecf886a5 | |||
| 0120d6eb49 | |||
| decc7ebe1d | |||
| fc4921087f | |||
| 98cbafd11a | |||
| bba3b8ba98 | |||
| 460f1ef45a | |||
| 2cf95b27c8 | |||
| 700b72b0d6 | |||
| 4c6d8ec80b | |||
| 96fc06b9e4 | |||
| 167ca22107 | |||
| 6681a0971a | |||
| 1d795ef975 | |||
| 4faafdf3e9 | |||
| 56df17ce25 | |||
| e82d44e94a | |||
| 7a70fcf114 | |||
| 3d35b77c23 | |||
| ec698505ec | |||
| b940dd4233 | |||
| 39e92203dc | |||
| 3fc2364eea | |||
| 801f8f0f83 | |||
| a565dce4a0 | |||
| 4806f2fa17 | |||
| a0bc2287ff |
@@ -21,8 +21,8 @@ changes, architectural rewrites. Those go to the human.
|
||||
|
||||
## Work queue (ranked)
|
||||
|
||||
1. **Fix AtlasCloud agent harness missing conformance marker** ([#4486](https://github.com/micro/go-micro/issues/4486)) — With the AtlasCloud delegate fallback shipped in #4493 and issue #4487 closed, the remaining Now-phase live-conformance gap is the scheduled agent harness exhausting retries without emitting the required marker. This stays first because the same provider-backed agent/model+tool contract must be dependable before higher-level services → agents → workflows demos can be trusted across providers.
|
||||
2. **Add first-agent docs command parity check** ([#4495](https://github.com/micro/go-micro/issues/4495)) — The current adoption goal says the queue must not collapse into internal hardening only. README, website docs, and recent blog posts now tell a coherent no-secret first-agent story (`micro agent demo` → examples → zero-to-hero), but that story can drift from CLI output and maintained example paths. Add a provider-free, CI-verifiable parity check so the 0→1/0→hero on-ramp remains walkable as the CLI and docs evolve.
|
||||
1. **Add first-agent failure-mode quick checks to the CLI on-ramp** ([#4591](https://github.com/micro/go-micro/issues/4591)) — The adoption goal is still the highest-value gap: a new developer should recover when scaffold → run → chat → inspect stalls without reading the whole docs tree. Keep this first because the README, website, and blog now tell a coherent services → agents → workflows story; the next increment should make the CLI on-ramp self-rescuing with provider-free, CI-verified debugging breadcrumbs.
|
||||
2. **Make AtlasCloud universe A2A reachability probe deterministic** ([#4504](https://github.com/micro/go-micro/issues/4504)) — With resume breadcrumbs shipped and the universe notify timeout gap closed, the remaining live AtlasCloud seam is the A2A reachability probe intermittently timing out. Keep this next so agent reachability over the interop gateway does not produce false negatives once the durable services → agents → workflows path itself has finalized.
|
||||
|
||||
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
|
||||
architecture-review pass._
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
name: govulncheck
|
||||
|
||||
# Deterministic vulnerability gate: runs govulncheck (reachability-aware CVE
|
||||
# scanner) on every push/PR. Fails on any reachable vulnerability EXCEPT the
|
||||
# explicit ALLOWLIST of known-unfixable ones, so a new vuln breaks the build
|
||||
# while tracked, no-upstream-fix ones don't. This is the gate the loop's
|
||||
# `security` role sits on top of — the role audits; this blocks known CVEs.
|
||||
#
|
||||
# Make this a required status check on the default branch to enforce it.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ["**"]
|
||||
pull_request:
|
||||
branches: ["**"]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
govulncheck:
|
||||
name: govulncheck
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: "1.25"
|
||||
check-latest: true
|
||||
- name: Install govulncheck
|
||||
run: go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- name: Scan
|
||||
env:
|
||||
# Reachable vulnerabilities with NO upstream fix, accepted for now and
|
||||
# tracked for remediation. Remove an ID the moment its fix lands.
|
||||
# GO-2026-5004 github.com/jackc/pgx/v4 -> pgx v5 migration (#4556)
|
||||
# GO-2026-4518 github.com/jackc/pgproto3/v2 -> pgx v5 migration (#4556)
|
||||
ALLOWLIST: "GO-2026-5004 GO-2026-4518"
|
||||
run: |
|
||||
out=$(mktemp)
|
||||
govulncheck ./... >"$out" 2>&1 && code=0 || code=$?
|
||||
cat "$out"
|
||||
if [ "$code" -eq 0 ]; then
|
||||
echo "govulncheck: no reachable vulnerabilities."
|
||||
exit 0
|
||||
fi
|
||||
if [ "$code" -ne 3 ]; then
|
||||
echo "::error::govulncheck failed to run (exit $code)."
|
||||
exit 1
|
||||
fi
|
||||
found=$(grep -oE 'Vulnerability #[0-9]+: GO-[0-9]{4}-[0-9]+' "$out" | grep -oE 'GO-[0-9]{4}-[0-9]+' | sort -u)
|
||||
unexpected=""
|
||||
for id in $found; do
|
||||
case " $ALLOWLIST " in
|
||||
*" $id "*) ;;
|
||||
*) unexpected="$unexpected $id" ;;
|
||||
esac
|
||||
done
|
||||
if [ -n "$unexpected" ]; then
|
||||
echo "::error::Unexpected reachable vulnerabilities:$unexpected"
|
||||
echo "If a fix exists, bump the dependency/toolchain. If genuinely unfixable, add the ID to ALLOWLIST with a tracking issue."
|
||||
exit 1
|
||||
fi
|
||||
echo "govulncheck: only allow-listed (known-unfixable) vulnerabilities present:$found"
|
||||
echo "OK."
|
||||
@@ -21,7 +21,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: 1.24
|
||||
go-version: "1.25"
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: golangci-lint
|
||||
|
||||
@@ -8,7 +8,7 @@ name: "Loop: Triage"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Harness (E2E)", "Lint", "Run Tests"]
|
||||
workflows: ["Harness (E2E)", "Lint", "Run Tests", "govulncheck"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.24
|
||||
go-version: "1.25"
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Get dependencies
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: 1.24
|
||||
go-version: "1.25"
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Get dependencies
|
||||
|
||||
@@ -17,21 +17,48 @@ below is kept current between tags and rolled into the next version when it ship
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **First-agent guide chain contract** — the harness now verifies the install → demo → examples → 0→hero guide chain stays connected for new agent builders. (`internal/harness/`, `internal/website/docs/`)
|
||||
|
||||
### Fixed
|
||||
- **A2A fallback artifact text** — A2A fallback responses now avoid leaking provider artifact text into agent-visible output. (`gateway/a2a/`, `agent/`)
|
||||
- **Launch readiness notification replays** — launch-readiness notification replay paths now deduplicate repeated side effects. (`agent/`, `internal/harness/`)
|
||||
- **Plan-delegate harness cleanup** — plan/delegate harness cleanup is more reliable after conformance runs. (`internal/harness/`)
|
||||
- **AtlasCloud spoken notify replays** — AtlasCloud fallback handling now collapses spoken notification replays more consistently. (`ai/atlascloud/`, `agent/`)
|
||||
- **Agent-flow onboarding side effects** — onboarding side-effect checks are more stable across the agent-flow harness. (`agent/`, `internal/harness/`)
|
||||
|
||||
---
|
||||
|
||||
## [6.5.0] - July 2026
|
||||
|
||||
### Added
|
||||
- **Agent stream provider conformance** — provider conformance now covers agent streaming behavior so streaming-capable providers stay aligned with the harness contract. (`agent/`, `internal/harness/`)
|
||||
- **First-agent docs CLI parity check** — the harness now verifies first-agent docs commands match the CLI wayfinding surface. (`internal/harness/`, `internal/website/docs/`)
|
||||
- **Focused CLI inner-loop contract** — the local harness now covers scaffold, run/chat/inspect, and deploy dry-run boundaries in one first-run contract. (`internal/harness/`)
|
||||
- **First-agent wayfinding breadcrumbs** — first-agent docs and examples now have locked breadcrumb coverage from the README through the runnable examples. (`README.md`, `internal/website/docs/`, `examples/`)
|
||||
- **Offline `micro new` contract** — project scaffolding now has an offline contract so the first service path stays runnable without network access. (`cmd/micro/`, `internal/harness/`)
|
||||
|
||||
### Changed
|
||||
- **Provider model call timeouts** — model call timeout enforcement now wraps provider calls more defensively, reducing hangs in agent and harness paths. (`agent/`, `ai/`)
|
||||
- **First-agent harness diagnostics** — getting-started harness logs now make first-run and 0→hero failures easier to locate. (`internal/harness/`)
|
||||
- **MiniMax streaming conformance** — MiniMax streaming coverage now exercises broader provider conformance behavior. (`ai/minimax/`, `internal/harness/`)
|
||||
- **AtlasCloud streaming tool capability** — AtlasCloud tool-streaming capability detection is now aligned with provider fallback behavior. (`ai/atlascloud/`, `agent/`)
|
||||
|
||||
### Fixed
|
||||
- **Partial text tool calls** — text tool-call recovery now repairs partial function-style calls more reliably before fallback parsing continues. (`agent/`)
|
||||
- **Retry timeout test stability** — retry timeout coverage is less race-prone. (`agent/`)
|
||||
- **Checkpointed tool-call resume** — resumed agent runs now preserve checkpointed tool calls across startup resume paths. (`agent/`)
|
||||
- **Model retry backoff contracts** — retry backoff behavior now has focused contract coverage for model-call failures. (`agent/`, `ai/`)
|
||||
- **AtlasCloud conformance markers** — AtlasCloud fallback paths now preserve conformance markers through tool-call recovery. (`ai/atlascloud/`, `agent/`)
|
||||
- **AtlasCloud delegate text fallback** — delegate text fallback recovery is more reliable for AtlasCloud responses. (`ai/atlascloud/`, `agent/`)
|
||||
- **AtlasCloud incomplete plan repairs** — incomplete plan repair paths now recover more consistently in AtlasCloud fallback handling. (`ai/atlascloud/`, `agent/`)
|
||||
- **AtlasCloud partial text tool calls** — AtlasCloud fallback handling now repairs partial text-rendered tool calls more reliably. (`ai/atlascloud/`, `agent/`)
|
||||
|
||||
### Documentation
|
||||
- **Roadmap agent status** — public roadmap docs now reflect the current agent lifecycle status more consistently. (`internal/website/docs/`)
|
||||
- **Agent resume limits** — docs now describe checkpoint resume boundaries for agent runs. (`internal/website/docs/`)
|
||||
- **Zero-to-hero harness boundaries** — docs now clarify which 0→hero lifecycle checks are maintained by the local harness. (`internal/website/docs/`, `internal/harness/`)
|
||||
- **First-agent wayfinding guard** — first-agent docs wayfinding now has tighter guard coverage around the README, docs, and examples chain. (`README.md`, `internal/website/docs/`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ help:
|
||||
@echo " make harness - Run deterministic getting-started and end-to-end harnesses"
|
||||
@echo " make inner-loop - Verify scaffold → run/chat/inspect → deploy dry-run contract"
|
||||
@echo " make cli-wayfinding - Verify installed first-agent CLI wayfinding commands"
|
||||
@echo " make docs-wayfinding - Verify first-agent docs wayfinding links resolve locally"
|
||||
@echo " make docs-wayfinding - Verify first-agent docs/CLI wayfinding stays in sync"
|
||||
@echo " make install-smoke - Verify the local install.sh and first-run CLI smoke path"
|
||||
@echo " make provider-conformance-mock - Run cross-provider harness with deterministic mock provider"
|
||||
@echo " make provider-conformance - Run harnesses against configured live providers"
|
||||
@@ -80,7 +80,8 @@ cli-wayfinding:
|
||||
# maintained local docs and examples. This is a focused no-network guard for the
|
||||
# developer-adoption on-ramp.
|
||||
docs-wayfinding:
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestFirstAgentWayfindingDocs|TestFirstAgentWayfindingLinkTargetsResolve' -count=1
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestFirstAgentWayfinding' -count=1
|
||||
go test ./cmd/micro -run 'TestFirstAgentDocsMatchCLIOutput|TestFirstAgentWalkthroughCLIBoundaries' -count=1
|
||||
|
||||
# Verify the documented install script and first-run CLI command boundaries without
|
||||
# provider keys or network access.
|
||||
|
||||
+17
-2
@@ -106,6 +106,11 @@ type agentImpl struct {
|
||||
// durable delegate-result cache is written.
|
||||
delegateMu sync.Mutex
|
||||
delegateCalls map[string]*delegateCall
|
||||
|
||||
// stopCh lets Stop unblock Run. Without this, tests and harnesses that
|
||||
// start agents in goroutines can leave Run parked forever after the RPC
|
||||
// server has been stopped.
|
||||
stopCh chan struct{}
|
||||
}
|
||||
|
||||
// New creates a new Agent.
|
||||
@@ -540,6 +545,11 @@ func (a *agentImpl) Run() error {
|
||||
return fmt.Errorf("failed to start agent: %w", err)
|
||||
}
|
||||
|
||||
stopCh := make(chan struct{})
|
||||
a.mu.Lock()
|
||||
a.stopCh = stopCh
|
||||
a.mu.Unlock()
|
||||
|
||||
fmt.Printf("Agent %s registered (manages: %s)\n", a.opts.Name, strings.Join(a.opts.Services, ", "))
|
||||
|
||||
// Optionally serve the agent directly over the A2A protocol, calling
|
||||
@@ -561,12 +571,17 @@ func (a *agentImpl) Run() error {
|
||||
fmt.Printf("Agent %s serving A2A on %s\n", a.opts.Name, a.opts.A2AAddress)
|
||||
}
|
||||
|
||||
ch := make(chan struct{})
|
||||
<-ch
|
||||
<-stopCh
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *agentImpl) Stop() error {
|
||||
a.mu.Lock()
|
||||
if a.stopCh != nil {
|
||||
close(a.stopCh)
|
||||
a.stopCh = nil
|
||||
}
|
||||
a.mu.Unlock()
|
||||
if a.server != nil {
|
||||
return a.server.Stop()
|
||||
}
|
||||
|
||||
+16
-5
@@ -737,17 +737,28 @@ func normalizeDelegateTask(task string) string {
|
||||
}
|
||||
}, task)
|
||||
task = strings.Join(strings.Fields(task), " ")
|
||||
if strings.Contains(task, "notify") &&
|
||||
strings.Contains(task, "owner") &&
|
||||
if 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")) {
|
||||
isLaunchReadinessDelegateTask(task) {
|
||||
return "notify owner@acme.com launch-plan-ready"
|
||||
}
|
||||
return task
|
||||
}
|
||||
|
||||
func isLaunchReadinessDelegateTask(task string) bool {
|
||||
hasNotify := strings.Contains(task, "notify") || strings.Contains(task, "notification") || strings.Contains(task, "tell")
|
||||
hasLaunch := strings.Contains(task, "launch")
|
||||
hasPlanOrReadiness := strings.Contains(task, "plan") || strings.Contains(task, "readiness") || strings.Contains(task, "ready")
|
||||
hasCompletion := strings.Contains(task, "ready") ||
|
||||
strings.Contains(task, "readiness") ||
|
||||
strings.Contains(task, "prepared") ||
|
||||
strings.Contains(task, "complete") ||
|
||||
strings.Contains(task, "finished") ||
|
||||
strings.Contains(task, "done") ||
|
||||
strings.Contains(task, "sent")
|
||||
return hasNotify && hasLaunch && hasPlanOrReadiness && hasCompletion
|
||||
}
|
||||
|
||||
// isAgent reports whether name resolves to a registered agent (a
|
||||
// service advertising type=agent in its metadata).
|
||||
func (a *agentImpl) isAgent(name string) bool {
|
||||
|
||||
+14
-9
@@ -196,16 +196,21 @@ func TestDelegateResultCacheReusesLaunchReadinessParaphrases(t *testing.T) {
|
||||
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")
|
||||
replayedTasks := []string{
|
||||
"Notify the plan owner at owner @ acme.com that launch readiness is prepared and complete.",
|
||||
"Tell owner at acme dot com the launch readiness notification was sent and the plan is done.",
|
||||
}
|
||||
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)
|
||||
for i, replayedTask := range replayedTasks {
|
||||
cached, ok := a.cachedDelegateResult("delegate-replay", " COMMS ", replayedTask)
|
||||
if !ok {
|
||||
t.Fatalf("cachedDelegateResult missed equivalent launch-readiness delegate replay %d", i)
|
||||
}
|
||||
if cached.ID != "delegate-replay" {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+54
-20
@@ -22,17 +22,20 @@ type conformanceProvider struct {
|
||||
live bool
|
||||
}
|
||||
|
||||
var agentConformanceProviders = []conformanceProvider{
|
||||
{name: "fake"},
|
||||
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
|
||||
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
|
||||
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
|
||||
{name: "gemini", key: "GEMINI_API_KEY", model: "GO_MICRO_CONFORMANCE_GEMINI_MODEL", live: true},
|
||||
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
|
||||
{name: "minimax", key: "MINIMAX_API_KEY", model: "GO_MICRO_CONFORMANCE_MINIMAX_MODEL", live: true},
|
||||
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
|
||||
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceMatrix(t *testing.T) {
|
||||
providers := []conformanceProvider{
|
||||
{name: "fake"},
|
||||
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
|
||||
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
|
||||
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
|
||||
{name: "gemini", key: "GEMINI_API_KEY", model: "GO_MICRO_CONFORMANCE_GEMINI_MODEL", live: true},
|
||||
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
|
||||
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
|
||||
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
|
||||
}
|
||||
providers := agentConformanceProviders
|
||||
|
||||
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
|
||||
for _, provider := range providers {
|
||||
@@ -47,16 +50,7 @@ func TestAgentProviderConformanceMatrix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
|
||||
providers := []conformanceProvider{
|
||||
{name: "fake"},
|
||||
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
|
||||
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
|
||||
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
|
||||
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
|
||||
{name: "minimax", key: "MINIMAX_API_KEY", model: "GO_MICRO_CONFORMANCE_MINIMAX_MODEL", live: true},
|
||||
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
|
||||
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
|
||||
}
|
||||
providers := streamConformanceProviders()
|
||||
|
||||
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
|
||||
for _, provider := range providers {
|
||||
@@ -70,6 +64,46 @@ func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func streamConformanceProviders() []conformanceProvider {
|
||||
providers := make([]conformanceProvider, 0, len(agentConformanceProviders))
|
||||
for _, provider := range agentConformanceProviders {
|
||||
// Gemini is covered by the non-streaming agent/tool matrix, but does not
|
||||
// currently advertise streaming in the provider capability registry.
|
||||
if provider.name == "gemini" {
|
||||
continue
|
||||
}
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
return providers
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceMatrixIncludesEveryLiveProvider(t *testing.T) {
|
||||
want := map[string]string{
|
||||
"openai": "OPENAI_API_KEY",
|
||||
"anthropic": "ANTHROPIC_API_KEY",
|
||||
"atlascloud": "ATLASCLOUD_API_KEY",
|
||||
"gemini": "GEMINI_API_KEY",
|
||||
"groq": "GROQ_API_KEY",
|
||||
"minimax": "MINIMAX_API_KEY",
|
||||
"mistral": "MISTRAL_API_KEY",
|
||||
"together": "TOGETHER_API_KEY",
|
||||
}
|
||||
got := map[string]string{}
|
||||
for _, provider := range agentConformanceProviders {
|
||||
if provider.live {
|
||||
got[provider.name] = provider.key
|
||||
}
|
||||
}
|
||||
for name, key := range want {
|
||||
if got[name] != key {
|
||||
t.Fatalf("agentConformanceProviders[%q] key = %q, want %q", name, got[name], key)
|
||||
}
|
||||
}
|
||||
if len(got) != len(want) {
|
||||
t.Fatalf("agentConformanceProviders live providers = %#v, want exactly %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func runAgentStreamConformanceScenario(t *testing.T, provider conformanceProvider) {
|
||||
t.Helper()
|
||||
if provider.live {
|
||||
|
||||
@@ -252,6 +252,114 @@ func TestGenerateWithRetryCapsRetryAfter(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithRetryDoesNotRetryPermanentProviderErrors(t *testing.T) {
|
||||
attempts := 0
|
||||
model := retryModel{generate: func(context.Context, *Request, ...GenerateOption) (*Response, error) {
|
||||
attempts++
|
||||
return nil, statusErr(400)
|
||||
}}
|
||||
|
||||
_, err := GenerateWithRetry(context.Background(), model, &Request{Prompt: "hi"}, GeneratePolicy{
|
||||
MaxAttempts: 3,
|
||||
Backoff: time.Millisecond,
|
||||
})
|
||||
if !errors.Is(err, statusErr(400)) {
|
||||
t.Fatalf("error = %v, want original provider status", err)
|
||||
}
|
||||
var retryErr *RetryError
|
||||
if errors.As(err, &retryErr) {
|
||||
t.Fatalf("error = %T %[1]v, want permanent provider error without retry wrapper", err)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Fatalf("attempts = %d, want no retry for permanent provider errors", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithRetryDefaultsToSingleAttempt(t *testing.T) {
|
||||
attempts := 0
|
||||
model := retryModel{generate: func(context.Context, *Request, ...GenerateOption) (*Response, error) {
|
||||
attempts++
|
||||
return nil, errors.New("temporary provider outage")
|
||||
}}
|
||||
|
||||
_, err := GenerateWithRetry(context.Background(), model, &Request{Prompt: "hi"}, GeneratePolicy{
|
||||
Backoff: time.Millisecond,
|
||||
})
|
||||
var retryErr *RetryError
|
||||
if !errors.As(err, &retryErr) {
|
||||
t.Fatalf("error = %T %[1]v, want retry error for exhausted transient attempt", err)
|
||||
}
|
||||
if retryErr.Attempts != 1 {
|
||||
t.Fatalf("retry attempts = %d, want default single attempt", retryErr.Attempts)
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Fatalf("model attempts = %d, want default single attempt", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithRetryStopsDuringBackoffWhenCallerCancels(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
var attempts atomic.Int32
|
||||
model := retryModel{generate: func(context.Context, *Request, ...GenerateOption) (*Response, error) {
|
||||
attempts.Add(1)
|
||||
return nil, statusErr(503)
|
||||
}}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := GenerateWithRetry(ctx, model, &Request{Prompt: "hi"}, GeneratePolicy{
|
||||
MaxAttempts: 3,
|
||||
Backoff: time.Hour,
|
||||
})
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
deadline := time.After(time.Second)
|
||||
for attempts.Load() == 0 {
|
||||
select {
|
||||
case err := <-errc:
|
||||
t.Fatalf("GenerateWithRetry returned before first attempt cancellation: %v", err)
|
||||
case <-deadline:
|
||||
t.Fatal("provider was not called")
|
||||
default:
|
||||
time.Sleep(time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
start := time.Now()
|
||||
cancel()
|
||||
select {
|
||||
case err := <-errc:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("error = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(200 * time.Millisecond):
|
||||
t.Fatal("GenerateWithRetry did not stop promptly during backoff cancellation")
|
||||
}
|
||||
if elapsed := time.Since(start); elapsed > 200*time.Millisecond {
|
||||
t.Fatalf("backoff cancellation took %s, want prompt return", elapsed)
|
||||
}
|
||||
if got := attempts.Load(); got != 1 {
|
||||
t.Fatalf("attempts = %d, want cancellation before retry", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRetryBackoffUsesExponentialBaseAndCap(t *testing.T) {
|
||||
if got := retryBackoff(statusErr(503), 1, 10*time.Millisecond); got != 10*time.Millisecond {
|
||||
t.Fatalf("attempt 1 backoff = %s, want 10ms", got)
|
||||
}
|
||||
if got := retryBackoff(statusErr(503), 2, 10*time.Millisecond); got != 20*time.Millisecond {
|
||||
t.Fatalf("attempt 2 backoff = %s, want 20ms", got)
|
||||
}
|
||||
if got := retryBackoff(statusErr(503), 3, 10*time.Millisecond); got != 40*time.Millisecond {
|
||||
t.Fatalf("attempt 3 backoff = %s, want 40ms", got)
|
||||
}
|
||||
if got := retryBackoff(statusErr(503), 20, time.Second); got != 30*time.Second {
|
||||
t.Fatalf("large backoff = %s, want 30s cap", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestHTTPErrorExposesStatusAndRetryAfter(t *testing.T) {
|
||||
resp := &http.Response{
|
||||
Status: "429 Too Many Requests",
|
||||
|
||||
+1
-1
@@ -684,7 +684,7 @@ micro loop init \
|
||||
```
|
||||
|
||||
- `--roles`: which roles to scaffold (`planner,builder,triage`, or `all`)
|
||||
- `--agent`: how the workflows summon the agent (an `@mention`)
|
||||
- `--agent`: how the workflows summon the agent — any `@mention`-driven coding agent (e.g. `@codex`, `@claude`)
|
||||
- `--token-secret`: repo secret holding the driving user PAT
|
||||
- `--branch`: base branch for the loop's PRs
|
||||
- `--ci-workflow`: `name:` of the CI workflow triage watches
|
||||
|
||||
@@ -14,6 +14,32 @@ import (
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
const firstAgentQuickChecksHelp = `First-agent failure-mode quick checks
|
||||
|
||||
Use this when scaffold -> run -> chat -> inspect stalls and you want the
|
||||
smallest provider-free recovery loop before reading the full docs.
|
||||
|
||||
1. Confirm prerequisites before starting the gateway:
|
||||
micro agent preflight
|
||||
|
||||
2. Start the project and keep it running in a separate terminal:
|
||||
micro run
|
||||
|
||||
3. Check the agent is registered and the chat gateway is reachable:
|
||||
micro agent doctor
|
||||
|
||||
4. If chat returns an answer or an error, inspect the latest run state:
|
||||
micro inspect agent <name>
|
||||
micro runs <name>
|
||||
|
||||
5. If provider chat is not configured yet, prove the no-secret path still works:
|
||||
micro agent demo
|
||||
go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1
|
||||
|
||||
Recovery docs:
|
||||
https://go-micro.dev/docs/guides/debugging-agents.html
|
||||
https://go-micro.dev/docs/guides/no-secret-first-agent.html`
|
||||
|
||||
const noSecretDemoHelp = `No-secret first-agent demo
|
||||
|
||||
Use this when you want the fastest provider-free agent success path before
|
||||
@@ -72,6 +98,18 @@ for live-provider chat and inspect/debugging.`,
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "quickcheck",
|
||||
Aliases: []string{"debug"},
|
||||
Usage: "Print first-agent failure-mode quick checks",
|
||||
Description: `Print provider-free recovery breadcrumbs for the scaffold -> run ->
|
||||
chat -> inspect loop, including exact commands for registration, gateway, run
|
||||
history, and no-secret fallback checks.`,
|
||||
Action: func(c *cli.Context) error {
|
||||
fmt.Fprintln(c.App.Writer, firstAgentQuickChecksHelp)
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "preflight",
|
||||
Usage: "Check local prerequisites before the first provider-backed agent",
|
||||
@@ -224,14 +262,44 @@ func writeRunIndex(w io.Writer, name string, runs []goagent.RunSummary, asJSON b
|
||||
if run.TraceID != "" {
|
||||
line += " trace=" + shortTraceID(run.TraceID)
|
||||
}
|
||||
if run.Checkpoint != "" {
|
||||
line += " checkpoint=" + run.Checkpoint
|
||||
}
|
||||
if run.Stage != "" {
|
||||
line += " stage=" + run.Stage
|
||||
}
|
||||
if run.LastError != "" {
|
||||
line += " error=" + run.LastError
|
||||
}
|
||||
fmt.Fprintln(w, line)
|
||||
writeRunIndexBreadcrumbs(w, name, run)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeRunIndexBreadcrumbs(w io.Writer, name string, run goagent.RunSummary) {
|
||||
if run.Stage == "input-required" {
|
||||
fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID)
|
||||
fmt.Fprintf(w, " input: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the input-required run\n", run.RunID)
|
||||
return
|
||||
}
|
||||
if !isResumableRunSummary(run) {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID)
|
||||
fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID)
|
||||
fmt.Fprintf(w, " stream: call micro.ResumeStreamAsk(ctx, agent, %q) to resume with streaming events\n", run.RunID)
|
||||
}
|
||||
|
||||
func isResumableRunSummary(run goagent.RunSummary) bool {
|
||||
switch run.Status {
|
||||
case "running", "error", "failed", "refused":
|
||||
return run.Checkpoint != "done" || run.Stage != ""
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func printRunHistory(name, runID string, asJSON bool) error {
|
||||
events, err := goagent.LoadRunEvents(store.DefaultStore, name, runID)
|
||||
if err != nil {
|
||||
|
||||
@@ -59,6 +59,46 @@ func TestWriteRunIndexHumanIncludesStatusAndDuration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRunIndexIncludesResumeBreadcrumbs(t *testing.T) {
|
||||
runs := []goagent.RunSummary{{
|
||||
RunID: "run-failed",
|
||||
Agent: "runner",
|
||||
UpdatedAt: time.Date(2026, 6, 25, 12, 34, 56, 0, time.UTC),
|
||||
Events: 3,
|
||||
Status: "error",
|
||||
LastKind: "tool",
|
||||
Checkpoint: "failed",
|
||||
Stage: "ask",
|
||||
}}
|
||||
var out bytes.Buffer
|
||||
if err := writeRunIndex(&out, "runner", runs, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"checkpoint=failed", "stage=ask", `micro agent history runner run-failed`, `micro.AgentResume(ctx, agent, "run-failed")`, `micro.ResumeStreamAsk(ctx, agent, "run-failed")`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRunIndexInputRequiredUsesResumeInput(t *testing.T) {
|
||||
runs := []goagent.RunSummary{{RunID: "run-input", Agent: "runner", Status: "running", LastKind: "checkpoint", Checkpoint: "paused", Stage: "input-required"}}
|
||||
var out bytes.Buffer
|
||||
if err := writeRunIndex(&out, "runner", runs, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{`micro agent history runner run-input`, `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, `micro.AgentResume(ctx, agent, "run-input")`) || strings.Contains(got, "ResumeStreamAsk") {
|
||||
t.Fatalf("input-required run should point at ResumeInput only, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteRunHistoryHumanAndJSON(t *testing.T) {
|
||||
events := []goagent.RunEvent{{
|
||||
Time: time.Date(2026, 6, 25, 12, 34, 56, 7_000_000, time.UTC),
|
||||
|
||||
@@ -73,3 +73,24 @@ func TestRunAgentDoctorReportsActionableRecoveryFailures(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentQuickcheckPrintsProviderFreeFailureModeBreadcrumbs(t *testing.T) {
|
||||
got := firstAgentQuickChecksHelp
|
||||
for _, want := range []string{
|
||||
"First-agent failure-mode quick checks",
|
||||
"scaffold -> run -> chat -> inspect",
|
||||
"micro agent preflight",
|
||||
"micro run",
|
||||
"micro agent doctor",
|
||||
"micro inspect agent <name>",
|
||||
"micro runs <name>",
|
||||
"micro agent demo",
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
|
||||
"debugging-agents.html",
|
||||
"no-secret-first-agent.html",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("quickcheck output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,8 @@ package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
@@ -36,6 +38,9 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
|
||||
if !subcommands["agent"]["doctor"] {
|
||||
t.Fatal("first-agent walkthrough missing recovery boundary: agent doctor")
|
||||
}
|
||||
if !subcommands["agent"]["quickcheck"] {
|
||||
t.Fatal("first-agent walkthrough missing failure-mode boundary: agent quickcheck")
|
||||
}
|
||||
if !subcommands["inspect"]["agent"] {
|
||||
t.Fatal("first-agent walkthrough missing inspect boundary: inspect agent")
|
||||
}
|
||||
@@ -116,6 +121,28 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
quickcheck := subcommandByName(t, agent, "quickcheck")
|
||||
out.Reset()
|
||||
if err := quickcheck.Action(cli.NewContext(app, nil, nil)); err != nil {
|
||||
t.Fatalf("micro agent quickcheck failed: %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"First-agent failure-mode quick checks",
|
||||
"scaffold -> run -> chat -> inspect",
|
||||
"micro agent preflight",
|
||||
"micro run",
|
||||
"micro agent doctor",
|
||||
"micro inspect agent <name>",
|
||||
"micro runs <name>",
|
||||
"micro agent demo",
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentTranscript -count=1",
|
||||
"debugging-agents.html",
|
||||
} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("micro agent quickcheck output missing %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
|
||||
demo := subcommandByName(t, agent, "demo")
|
||||
out.Reset()
|
||||
if err := demo.Action(cli.NewContext(app, nil, nil)); err != nil {
|
||||
@@ -139,6 +166,126 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", ".."))
|
||||
outputs := map[string]string{
|
||||
"micro docs": commandOutput(t, commandByName(t, "docs")),
|
||||
"micro examples": commandOutput(t, commandByName(t, "examples")),
|
||||
"micro zero-to-hero": commandOutput(t, commandByName(t, "zero-to-hero")),
|
||||
}
|
||||
agent := commandByName(t, "agent")
|
||||
outputs["micro agent demo"] = commandOutput(t, subcommandByName(t, agent, "demo"))
|
||||
|
||||
contracts := []struct {
|
||||
name string
|
||||
file string
|
||||
markers []string
|
||||
}{
|
||||
{
|
||||
name: "README first-agent on-ramp",
|
||||
file: filepath.Join(root, "README.md"),
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"examples/first-agent/",
|
||||
"examples/support/",
|
||||
"internal/website/docs/guides/no-secret-first-agent.md",
|
||||
"internal/website/docs/guides/your-first-agent.md",
|
||||
"internal/website/docs/guides/debugging-agents.md",
|
||||
"internal/website/docs/guides/zero-to-hero.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "website getting-started first-agent on-ramp",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"github.com/micro/go-micro/tree/master/examples/first-agent",
|
||||
"github.com/micro/go-micro/tree/master/examples/support",
|
||||
"guides/no-secret-first-agent.html",
|
||||
"guides/your-first-agent.html",
|
||||
"guides/debugging-agents.html",
|
||||
"guides/zero-to-hero.html",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, contract := range contracts {
|
||||
doc := readTestFile(t, contract.file)
|
||||
for _, marker := range contract.markers {
|
||||
if !strings.Contains(doc, marker) {
|
||||
t.Fatalf("%s missing documented first-agent marker %q", contract.name, marker)
|
||||
}
|
||||
if isCLIContractMarker(marker) && !cliOutputsContain(outputs, marker) {
|
||||
t.Fatalf("%s documents %q, but none of the first-agent CLI outputs mention it; keep README/website breadcrumbs aligned with micro agent demo/examples/zero-to-hero", contract.name, marker)
|
||||
}
|
||||
assertMaintainedFirstAgentPath(t, root, marker)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func commandOutput(t *testing.T, command *cli.Command) string {
|
||||
t.Helper()
|
||||
var out bytes.Buffer
|
||||
app := cli.NewApp()
|
||||
app.Writer = &out
|
||||
if err := command.Action(cli.NewContext(app, nil, nil)); err != nil {
|
||||
t.Fatalf("%s failed: %v", command.Name, err)
|
||||
}
|
||||
return out.String()
|
||||
}
|
||||
|
||||
func cliOutputsContain(outputs map[string]string, marker string) bool {
|
||||
for command, out := range outputs {
|
||||
if command == marker || strings.Contains(out, marker) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isCLIContractMarker(marker string) bool {
|
||||
return strings.HasPrefix(marker, "micro ") || strings.HasPrefix(marker, "go run ") || strings.HasPrefix(marker, "go test ") || strings.Contains(marker, ".html")
|
||||
}
|
||||
|
||||
func assertMaintainedFirstAgentPath(t *testing.T, root, marker string) {
|
||||
t.Helper()
|
||||
pathChecks := map[string]string{
|
||||
"go run ./examples/first-agent": "examples/first-agent",
|
||||
"examples/first-agent/": "examples/first-agent",
|
||||
"examples/support/": "examples/support",
|
||||
"internal/website/docs/guides/no-secret-first-agent.md": "internal/website/docs/guides/no-secret-first-agent.md",
|
||||
"internal/website/docs/guides/your-first-agent.md": "internal/website/docs/guides/your-first-agent.md",
|
||||
"internal/website/docs/guides/debugging-agents.md": "internal/website/docs/guides/debugging-agents.md",
|
||||
"internal/website/docs/guides/zero-to-hero.md": "internal/website/docs/guides/zero-to-hero.md",
|
||||
"guides/no-secret-first-agent.html": "internal/website/docs/guides/no-secret-first-agent.md",
|
||||
"guides/your-first-agent.html": "internal/website/docs/guides/your-first-agent.md",
|
||||
"guides/debugging-agents.html": "internal/website/docs/guides/debugging-agents.md",
|
||||
"guides/zero-to-hero.html": "internal/website/docs/guides/zero-to-hero.md",
|
||||
"github.com/micro/go-micro/tree/master/examples/first-agent": "examples/first-agent",
|
||||
"github.com/micro/go-micro/tree/master/examples/support": "examples/support",
|
||||
}
|
||||
path, ok := pathChecks[marker]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(path))); err != nil {
|
||||
t.Fatalf("documented first-agent path %q from marker %q does not resolve: %v", path, marker, err)
|
||||
}
|
||||
}
|
||||
|
||||
func readTestFile(t *testing.T, path string) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
t.Fatalf("read %s: %v", path, err)
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
|
||||
func commandByName(t *testing.T, name string) *cli.Command {
|
||||
t.Helper()
|
||||
for _, command := range microcmd.DefaultCmd.App().Commands {
|
||||
|
||||
@@ -98,16 +98,25 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
|
||||
fmt.Fprintf(w, " trace=%s", shortID(run.TraceID))
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
if isResumableAgentRun(run) {
|
||||
fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID)
|
||||
}
|
||||
if run.Stage == "input-required" {
|
||||
fmt.Fprintf(w, " input: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the paused run\n", run.RunID)
|
||||
}
|
||||
writeAgentRunBreadcrumbs(w, name, run)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeAgentRunBreadcrumbs(w io.Writer, name string, run goagent.RunSummary) {
|
||||
if run.Stage == "input-required" {
|
||||
fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID)
|
||||
fmt.Fprintf(w, " input: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the input-required run\n", run.RunID)
|
||||
return
|
||||
}
|
||||
if !isResumableAgentRun(run) {
|
||||
return
|
||||
}
|
||||
fmt.Fprintf(w, " inspect: micro agent history %s %s\n", name, run.RunID)
|
||||
fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID)
|
||||
fmt.Fprintf(w, " stream: call micro.ResumeStreamAsk(ctx, agent, %q) to resume with streaming events\n", run.RunID)
|
||||
}
|
||||
|
||||
func isResumableAgentRun(run goagent.RunSummary) bool {
|
||||
switch run.Status {
|
||||
case "running", "error", "failed", "refused":
|
||||
|
||||
@@ -17,7 +17,7 @@ func TestWriteAgentInspectionIncludesActionableBreadcrumbs(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=error", "events=4", "last=tool", "checkpoint=failed", "stage=ask", `error="boom"`, "trace=1234567890ab", `micro.AgentResume(ctx, agent, "run-1")`} {
|
||||
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=error", "events=4", "last=tool", "checkpoint=failed", "stage=ask", `error="boom"`, "trace=1234567890ab", `micro agent history support run-1`, `micro.AgentResume(ctx, agent, "run-1")`, `micro.ResumeStreamAsk(ctx, agent, "run-1")`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
@@ -31,11 +31,14 @@ func TestWriteAgentInspectionIncludesInputResumeBreadcrumb(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
|
||||
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro agent history support run-input`, `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
if strings.Contains(got, `micro.AgentResume(ctx, agent, "run-input")`) || strings.Contains(got, "ResumeStreamAsk") {
|
||||
t.Fatalf("input-required run should point at ResumeInput only, got:\n%s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteAgentInspectionEmptyStateNamesInspectCommand(t *testing.T) {
|
||||
|
||||
@@ -123,7 +123,7 @@ Examples:
|
||||
&cli.StringFlag{Name: "dir", Usage: "Target repo directory", Value: "."},
|
||||
&cli.StringFlag{Name: "roles", Usage: "Comma-separated roles, or 'all'", Value: "planner,builder,triage"},
|
||||
&cli.StringFlag{Name: "branch", Usage: "Base branch for the loop's PRs (auto-detected if empty)"},
|
||||
&cli.StringFlag{Name: "agent", Usage: "How the workflows summon the agent (an @mention)", Value: "@codex"},
|
||||
&cli.StringFlag{Name: "agent", Usage: "How the workflows summon the agent — any @mention-driven coding agent (e.g. @codex, @claude)", Value: "@codex"},
|
||||
&cli.StringFlag{Name: "token-secret", Usage: "Repo secret holding the user PAT that drives dispatch", Value: "LOOP_TOKEN"},
|
||||
&cli.StringFlag{Name: "ci-workflow", Usage: "CI workflow name(s) triage watches for failures (comma-separated)", Value: "CI"},
|
||||
&cli.StringFlag{Name: "planner-cron", Usage: "Cron schedule for the planner", Value: "0 * * * *"},
|
||||
|
||||
+35
-5
@@ -627,6 +627,9 @@ func (d *dispatcher) run(ctx context.Context, params json.RawMessage, invoke Inv
|
||||
reply = err.Error()
|
||||
state = stateInputRequired
|
||||
}
|
||||
} else if strings.TrimSpace(reply) == "" {
|
||||
reply = "error: agent returned an empty response"
|
||||
state = stateFailed
|
||||
}
|
||||
task := d.taskFromReply(p.Message, reply, state)
|
||||
d.store(task)
|
||||
@@ -750,13 +753,11 @@ func (g *Gateway) callAgent(ctx context.Context, name, message string) (string,
|
||||
if err := g.opts.Client.Call(ctx, req, &rsp); err != nil {
|
||||
return "", err
|
||||
}
|
||||
var out struct {
|
||||
Reply string `json:"reply"`
|
||||
}
|
||||
if err := json.Unmarshal(rsp.Data, &out); err != nil {
|
||||
reply, err := decodeAgentChatReply(rsp.Data)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return out.Reply, nil
|
||||
return reply, nil
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -976,6 +977,35 @@ func textArtifact(text string) Artifact {
|
||||
}
|
||||
}
|
||||
|
||||
func decodeAgentChatReply(data []byte) (string, error) {
|
||||
var out struct {
|
||||
Reply string `json:"reply"`
|
||||
Answer string `json:"answer"`
|
||||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
Message struct {
|
||||
Content string `json:"content"`
|
||||
Text string `json:"text"`
|
||||
} `json:"message"`
|
||||
}
|
||||
if err := json.Unmarshal(data, &out); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for _, candidate := range []string{
|
||||
out.Reply,
|
||||
out.Answer,
|
||||
out.Content,
|
||||
out.Text,
|
||||
out.Message.Content,
|
||||
out.Message.Text,
|
||||
} {
|
||||
if strings.TrimSpace(candidate) != "" {
|
||||
return candidate, nil
|
||||
}
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
// requestContext carries request cancellation and deadlines into the downstream
|
||||
// agent call without leaking HTTP transport context values into the go-micro
|
||||
// client stack.
|
||||
|
||||
@@ -523,6 +523,65 @@ func TestMessageStreamChunksFallsBackWhenUnsupported(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMessageStreamFallbackDoesNotCompleteWithEmptyText(t *testing.T) {
|
||||
d := newDispatcher()
|
||||
body := `{"jsonrpc":"2.0","id":1,"method":"message/stream","params":{"message":{"role":"user","parts":[{"kind":"text","text":"ping"}],"kind":"message"}}}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/", bytes.NewBufferString(body))
|
||||
rr := httptest.NewRecorder()
|
||||
|
||||
d.serveWithStream(rr, req, func(context.Context, string) (string, error) {
|
||||
return "", nil
|
||||
}, func(context.Context, string) (ai.Stream, error) {
|
||||
return nil, fmt.Errorf("%w: test provider", ai.ErrStreamingUnsupported)
|
||||
})
|
||||
|
||||
var event struct {
|
||||
Result Task `json:"result"`
|
||||
Error *rpcError `json:"error"`
|
||||
}
|
||||
for _, line := range strings.Split(strings.TrimSpace(rr.Body.String()), "\n") {
|
||||
line = strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(line), "data: "))
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
if err := json.Unmarshal([]byte(line), &event); err != nil {
|
||||
t.Fatalf("decode event %q: %v", line, err)
|
||||
}
|
||||
}
|
||||
if event.Error != nil {
|
||||
t.Fatalf("fallback event error: %+v", event.Error)
|
||||
}
|
||||
if event.Result.Status.State != stateFailed {
|
||||
t.Fatalf("fallback state = %q, want failed", event.Result.Status.State)
|
||||
}
|
||||
if got := textOf(event.Result.Artifacts[0].Parts); got == "" {
|
||||
t.Fatalf("fallback artifact text is empty: %+v", event.Result.Artifacts)
|
||||
}
|
||||
if got := textOf(event.Result.History[len(event.Result.History)-1].Parts); got == "" {
|
||||
t.Fatalf("fallback history text is empty: %+v", event.Result.History)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDecodeAgentChatReplyFallsBackToProviderTextFields(t *testing.T) {
|
||||
for name, body := range map[string]string{
|
||||
"answer": `{"answer":"answer text"}`,
|
||||
"content": `{"content":"content text"}`,
|
||||
"text": `{"text":"text field"}`,
|
||||
"message_content": `{"message":{"content":"message content"}}`,
|
||||
"message_text": `{"message":{"text":"message text"}}`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
got, err := decodeAgentChatReply([]byte(body))
|
||||
if err != nil {
|
||||
t.Fatalf("decodeAgentChatReply error: %v", err)
|
||||
}
|
||||
if strings.TrimSpace(got) == "" {
|
||||
t.Fatalf("decodeAgentChatReply(%s) returned empty text", body)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTasksResubscribeStreamsCurrentAndSubsequentEvents(t *testing.T) {
|
||||
d := newDispatcher()
|
||||
initial := &Task{ID: "task-1", ContextID: "ctx-1", Kind: "task", Status: TaskStatus{State: stateWorking, Timestamp: time.Now().UTC().Format(time.RFC3339)}}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module go-micro.dev/v6
|
||||
|
||||
go 1.24
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.24.1
|
||||
toolchain go1.25.12
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
@@ -32,24 +32,23 @@ require (
|
||||
github.com/prometheus/client_model v0.6.1
|
||||
github.com/rabbitmq/amqp091-go v1.10.0
|
||||
github.com/stretchr/objx v0.5.2
|
||||
github.com/stretchr/testify v1.10.0
|
||||
github.com/stretchr/testify v1.11.1
|
||||
github.com/test-go/testify v1.1.4
|
||||
github.com/urfave/cli/v2 v2.27.6
|
||||
github.com/xlab/treeprint v1.2.0
|
||||
go.etcd.io/bbolt v1.4.0
|
||||
go.etcd.io/etcd/api/v3 v3.5.21
|
||||
go.etcd.io/etcd/client/v3 v3.5.21
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.opentelemetry.io/otel v1.39.0
|
||||
go.opentelemetry.io/otel/sdk v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
go.uber.org/zap v1.27.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
golang.org/x/net v0.38.0
|
||||
golang.org/x/sync v0.13.0
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463
|
||||
google.golang.org/grpc v1.71.1
|
||||
golang.org/x/crypto v0.51.0
|
||||
golang.org/x/net v0.55.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/grpc v1.79.3
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057
|
||||
google.golang.org/protobuf v1.36.6
|
||||
google.golang.org/protobuf v1.36.10
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -63,7 +62,7 @@ require (
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/fatih/color v1.16.0 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-logr/logr v1.4.3 // indirect
|
||||
github.com/go-logr/stdr v1.2.2 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/google/go-tpm v0.9.3 // indirect
|
||||
@@ -97,19 +96,20 @@ require (
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/prometheus/common v0.63.0 // indirect
|
||||
github.com/prometheus/procfs v0.16.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.uber.org/multierr v1.10.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/mod v0.24.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/time v0.11.0 // indirect
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
@@ -65,8 +65,8 @@ github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9
|
||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
||||
github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A=
|
||||
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
@@ -317,8 +317,8 @@ github.com/rabbitmq/amqp091-go v1.10.0 h1:STpn5XsHlHGcecLmMFCtg7mqq0RnD+zFr4uzuk
|
||||
github.com/rabbitmq/amqp091-go v1.10.0/go.mod h1:Hy4jKW5kQART1u+JkDTF9YYOQUHXqMuhrgxOEeS7G4o=
|
||||
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ=
|
||||
github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU=
|
||||
github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc=
|
||||
@@ -345,8 +345,8 @@ github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81P
|
||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/test-go/testify v1.1.4 h1:Tf9lntrKUMHiXQ07qBScBTSA0dhYQlu83hswqelv1iE=
|
||||
github.com/test-go/testify v1.1.4/go.mod h1:rH7cfJo/47vWGdi4GPj16x3/t1xGOj2YxzmNQzk2ghU=
|
||||
github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM=
|
||||
@@ -368,18 +368,18 @@ go.etcd.io/etcd/client/pkg/v3 v3.5.21 h1:lPBu71Y7osQmzlflM9OfeIV2JlmpBjqBNlLtcoB
|
||||
go.etcd.io/etcd/client/pkg/v3 v3.5.21/go.mod h1:BgqT/IXPjK9NkeSDjbzwsHySX3yIle2+ndz28nVsjUs=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21 h1:T6b1Ow6fNjOLOtM0xSoKNQt1ASPCLWrF9XMHcH9pEyY=
|
||||
go.etcd.io/etcd/client/v3 v3.5.21/go.mod h1:mFYy67IOqmbRf/kRUvsHixzo3iG+1OF2W2+jVIQRAnU=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.35.0 h1:xKWKPxrxB6OtMCbmMY021CqC45J+3Onta9MqjhnusiQ=
|
||||
go.opentelemetry.io/otel v1.35.0/go.mod h1:UEqy8Zp11hpkUrL73gSlELM0DupHoiq72dR+Zqel/+Y=
|
||||
go.opentelemetry.io/otel/metric v1.35.0 h1:0znxYu2SNyuMSQT4Y9WDWej0VpcsxkuklLa4/siN90M=
|
||||
go.opentelemetry.io/otel/metric v1.35.0/go.mod h1:nKVFgxBZ2fReX6IlyW28MgZojkoAkJGaE8CpgeAU3oE=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0 h1:iPctf8iprVySXSKJffSS79eOjl9pvxV9ZqOWT0QejKY=
|
||||
go.opentelemetry.io/otel/sdk v1.35.0/go.mod h1:+ga1bZliga3DxJ3CQGg3updiaAJoNECOgJREo9KHGQg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0 h1:1RriWBmCKgkeHEhM7a2uMjMUfP7MsOF5JpUCaEqEI9o=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.35.0/go.mod h1:is6XYCUMpcKi+ZsOvfluY5YstFnhW0BidkR+gL+qN+w=
|
||||
go.opentelemetry.io/otel/trace v1.35.0 h1:dPpEfJu1sDIqruz7BHFG3c7528f6ddfSWfFDVt/xgMs=
|
||||
go.opentelemetry.io/otel/trace v1.35.0/go.mod h1:WUk7DtFp1Aw2MkvqGdwiXYDZZNvA/1J8o6xRXLrIkyc=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
|
||||
@@ -408,8 +408,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
|
||||
golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
@@ -418,8 +418,8 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
@@ -433,16 +433,16 @@ golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v
|
||||
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
|
||||
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
|
||||
golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
@@ -473,8 +473,8 @@ golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
@@ -482,8 +482,8 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0=
|
||||
golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -499,24 +499,26 @@ golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapK
|
||||
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
|
||||
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
|
||||
golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
|
||||
golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU=
|
||||
golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463 h1:hE3bRWtU6uceqlh4fhrSnUyjKHMKB9KrTLLG+bc0ddM=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20250324211829-b45e905df463/go.mod h1:U90ffi8eUL9MwPcrJylN5+Mk2v3vuPDptd5yyNUiRR8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 h1:e0AIkUUhxyBKh6ssZNrAMeqhA7RKUj42346d1y02i2g=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.71.1 h1:ffsFWr7ygTUscGPI0KKK6TLrGz0476KUvvsbqWK0rPI=
|
||||
google.golang.org/grpc v1.71.1/go.mod h1:H0GRtasmQOh9LkFoCPDu3ZrwUtD1YGE+b2vYBYd/8Ec=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls=
|
||||
google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057 h1:lPv+iqlAyiKMjbL3ivJlAASixPknLv806R6zaoE4PUM=
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057/go.mod h1:WPWnet+nYurNGpV0rVYHI1YuOJwVHeM3t8f76m410XM=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -3,8 +3,11 @@
|
||||
`go test ./...` includes `TestAgentProviderConformanceMatrix`, a shared agent
|
||||
scenario that runs against every registered chat provider. The scenario asks an
|
||||
agent to call a deterministic local tool, verifies the tool receives `ai.RunInfo`,
|
||||
and checks the final response carries the conformance marker. A fake provider path
|
||||
runs on every machine without network access so CI always exercises the harness.
|
||||
and checks the final response carries the conformance marker. The live matrix
|
||||
includes MiniMax in the tool/guardrail path in addition to providers with
|
||||
streaming coverage, so every supported chat provider has at least one key-gated
|
||||
agent contract. A fake provider path runs on every machine without network
|
||||
access so CI always exercises the harness.
|
||||
|
||||
Live providers are opt-in to avoid flaky unauthenticated PR checks and accidental
|
||||
API spend. To run the live matrix, set `GO_MICRO_AGENT_CONFORMANCE_LIVE=1` plus the
|
||||
|
||||
@@ -21,8 +21,8 @@ Actions instead of subagents. Each role is a workflow:
|
||||
|------|------------------------|--------------|
|
||||
| **Planner** | `loop-planner.yml` — *Loop: Planner* | Tracks live state, prioritizes the roadmap + an internal scan, and maintains the ranked queue in [`.github/loop/PRIORITIES.md`](../../.github/loop/PRIORITIES.md). Decides *what*. |
|
||||
| **Generator** | `loop-builder.yml` — *Loop: Builder (Generator)* | Builds the top open queue item as a single-concern PR (via Codex) and self-merges on green CI. Does the work. |
|
||||
| **Evaluator** | `harness.yml` — *Harness (E2E)*, plus the CI gate (`tests.yaml`, `lint.yaml`) | Grades every change: the mock harness + unit/lint on each push/PR, and real-model conformance hourly. A *separate* grader — never the generator judging itself. |
|
||||
| **Evaluator → feedback** | `loop-triage.yml` — *Loop: Triage (Evaluator feedback)* | When a gate workflow (Lint, Run Tests, or the harness) fails on a non-PR run, root-causes, dedupes, and files scoped fix issues back into the planner's queue. The hill-climbing feedback path. |
|
||||
| **Evaluator** | `harness.yml` — *Harness (E2E)*, plus the CI gate (`tests.yaml`, `lint.yaml`, `govulncheck.yml`) | Grades every change: the mock harness + unit/lint + reachable-CVE scan on each push/PR, and real-model conformance hourly. A *separate* grader — never the generator judging itself. |
|
||||
| **Evaluator → feedback** | `loop-triage.yml` — *Loop: Triage (Evaluator feedback)* | When a gate workflow (Lint, Run Tests, govulncheck, or the harness) fails on a non-PR run, root-causes, dedupes, and files scoped fix issues back into the planner's queue. The hill-climbing feedback path. |
|
||||
| **Coherence** | `loop-coherence.yml` — *Loop: Coherence* | Keeps README/website/docs/blog aligned with the North Star, keeps `CHANGELOG.md` living (reconciling `[Unreleased]` against merged PRs and rolling it into version headings as tags cut), and drafts the changelog blog post. |
|
||||
| **Security** | `loop-security.yml` — *Loop: Security* | Weekly vulnerability audit of the attack surface (MCP/A2A gateways, x402, auth, provider URLs, agent tool loop, deps via `govulncheck`). Files `security` issues; **never auto-merges** fixes and **never publishes exploit detail** in public issues (responsible disclosure); risky fixes are `needs-human`. |
|
||||
| **Release** | `loop-release.yml` — *Loop: Release (daily patch)* | Cuts a daily patch tag when master has new commits, so the *installable* framework tracks the loop's improvements (triggers `release.yml`/goreleaser). Minor/major bumps stay with the human. |
|
||||
|
||||
@@ -49,19 +49,31 @@ type CreateResponse struct {
|
||||
}
|
||||
|
||||
type WorkspaceService struct {
|
||||
mu sync.Mutex
|
||||
n int
|
||||
mu sync.Mutex
|
||||
n int
|
||||
byOwner map[string]*Workspace
|
||||
}
|
||||
|
||||
// Create provisions a workspace for a new user.
|
||||
// @example {"owner": "alice@acme.com"}
|
||||
func (s *WorkspaceService) Create(ctx context.Context, req *CreateRequest, rsp *CreateResponse) error {
|
||||
s.mu.Lock()
|
||||
if s.byOwner == nil {
|
||||
s.byOwner = make(map[string]*Workspace)
|
||||
}
|
||||
if ws, ok := s.byOwner[req.Owner]; ok {
|
||||
s.mu.Unlock()
|
||||
fmt.Printf(" \033[32m[workspace]\033[0m duplicate suppressed %s for %s\n", ws.ID, req.Owner)
|
||||
rsp.Workspace = ws
|
||||
return nil
|
||||
}
|
||||
s.n++
|
||||
id := fmt.Sprintf("ws-%d", s.n)
|
||||
ws := &Workspace{ID: id, Owner: req.Owner}
|
||||
s.byOwner[req.Owner] = ws
|
||||
s.mu.Unlock()
|
||||
fmt.Printf(" \033[32m[workspace]\033[0m created %s for %s\n", id, req.Owner)
|
||||
rsp.Workspace = &Workspace{ID: id, Owner: req.Owner}
|
||||
rsp.Workspace = ws
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -79,14 +91,26 @@ type SendResponse struct {
|
||||
Sent bool `json:"sent"`
|
||||
}
|
||||
type NotifyService struct {
|
||||
mu sync.Mutex
|
||||
n int
|
||||
mu sync.Mutex
|
||||
n int
|
||||
sent map[string]bool
|
||||
}
|
||||
|
||||
// Send delivers a notification message to a recipient.
|
||||
// @example {"to": "alice@acme.com", "message": "Welcome"}
|
||||
func (s *NotifyService) Send(ctx context.Context, req *SendRequest, rsp *SendResponse) error {
|
||||
key := strings.ToLower(strings.TrimSpace(req.To))
|
||||
s.mu.Lock()
|
||||
if s.sent == nil {
|
||||
s.sent = make(map[string]bool)
|
||||
}
|
||||
if s.sent[key] {
|
||||
s.mu.Unlock()
|
||||
fmt.Printf(" \033[35m[notify]\033[0m duplicate suppressed to=%s message=%q\n", req.To, req.Message)
|
||||
rsp.Sent = true
|
||||
return nil
|
||||
}
|
||||
s.sent[key] = true
|
||||
s.n++
|
||||
s.mu.Unlock()
|
||||
fmt.Printf(" \033[35m[notify]\033[0m 📨 to=%s message=%q\n", req.To, req.Message)
|
||||
@@ -178,15 +202,22 @@ func waitFor(reg registry.Registry, name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func waitForOnboardingSideEffects(ctx context.Context, wsSvc *WorkspaceService, ntSvc *NotifyService) error {
|
||||
func waitForOnboardingSideEffects(ctx context.Context, wsSvc *WorkspaceService, ntSvc *NotifyService, recoverMissingNotify func(context.Context) error) error {
|
||||
ticker := time.NewTicker(50 * time.Millisecond)
|
||||
defer ticker.Stop()
|
||||
|
||||
recovered := false
|
||||
for {
|
||||
workspaces, notifications := wsSvc.count(), ntSvc.count()
|
||||
if workspaces >= 1 && notifications >= 1 {
|
||||
return nil
|
||||
}
|
||||
if workspaces >= 1 && notifications == 0 && !recovered && recoverMissingNotify != nil {
|
||||
recovered = true
|
||||
if err := recoverMissingNotify(ctx); err != nil {
|
||||
return fmt.Errorf("agent-flow created workspace but failed to recover missing onboarding notification: workspaces=%d/1 notifications=%d/1: %w", workspaces, notifications, err)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
@@ -280,7 +311,11 @@ func main() {
|
||||
// effects. The 0→hero/provider conformance path must not print success
|
||||
// unless the services → agent → workflow contract actually happened.
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc)
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc, func(ctx context.Context) error {
|
||||
fmt.Print("\n\033[33mwarning:\033[0m workspace exists before notify; retrying the welcome notification once before the flow can complete.\n")
|
||||
_, err := onboarder.Ask(ctx, "The workspace for alice@acme.com already exists. Send exactly one welcome notification to alice@acme.com now. Use the notify service. Do not create another workspace and do not answer until the notification tool call has succeeded.")
|
||||
return err
|
||||
})
|
||||
cancel()
|
||||
|
||||
fmt.Printf("\n\033[1mresult:\033[0m workspaces created=%d, notifications sent=%d\n", wsSvc.count(), ntSvc.count())
|
||||
|
||||
@@ -109,7 +109,7 @@ func TestWaitForOnboardingSideEffectsFailsWhenMissing(t *testing.T) {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond)
|
||||
defer cancel()
|
||||
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc)
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc, nil)
|
||||
if err == nil {
|
||||
t.Fatal("waitForOnboardingSideEffects returned nil, want missing side effects error")
|
||||
}
|
||||
@@ -131,7 +131,81 @@ func TestWaitForOnboardingSideEffectsPassesWhenComplete(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc); err != nil {
|
||||
if err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc, nil); err != nil {
|
||||
t.Fatalf("waitForOnboardingSideEffects returned %v, want nil", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWaitForOnboardingSideEffectsRecoversMissingNotification(t *testing.T) {
|
||||
wsSvc := new(WorkspaceService)
|
||||
ntSvc := new(NotifyService)
|
||||
|
||||
if err := wsSvc.Create(context.Background(), &CreateRequest{Owner: "alice@acme.com"}, &CreateResponse{}); err != nil {
|
||||
t.Fatalf("create workspace: %v", err)
|
||||
}
|
||||
|
||||
recovered := false
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc, func(ctx context.Context) error {
|
||||
recovered = true
|
||||
return ntSvc.Send(ctx, &SendRequest{To: "alice@acme.com", Message: "Welcome — your workspace is ready."}, &SendResponse{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("waitForOnboardingSideEffects returned %v, want recovered notification", err)
|
||||
}
|
||||
if !recovered {
|
||||
t.Fatal("missing notification recovery did not run")
|
||||
}
|
||||
if got := ntSvc.count(); got != 1 {
|
||||
t.Fatalf("notifications sent = %d, want 1 after recovery", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWorkspaceCreateSuppressesDuplicateOwner(t *testing.T) {
|
||||
wsSvc := new(WorkspaceService)
|
||||
first := new(CreateResponse)
|
||||
if err := wsSvc.Create(context.Background(), &CreateRequest{Owner: "alice@acme.com"}, first); err != nil {
|
||||
t.Fatalf("create first workspace: %v", err)
|
||||
}
|
||||
second := new(CreateResponse)
|
||||
if err := wsSvc.Create(context.Background(), &CreateRequest{Owner: "alice@acme.com"}, second); err != nil {
|
||||
t.Fatalf("create duplicate workspace: %v", err)
|
||||
}
|
||||
|
||||
if got := wsSvc.count(); got != 1 {
|
||||
t.Fatalf("workspace creations = %d, want 1 after duplicate owner replay", got)
|
||||
}
|
||||
if first.Workspace == nil || second.Workspace == nil || second.Workspace.ID != first.Workspace.ID {
|
||||
t.Fatalf("duplicate create returned workspace %#v, want original %#v", second.Workspace, first.Workspace)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifySendSuppressesDuplicateMessage(t *testing.T) {
|
||||
ntSvc := new(NotifyService)
|
||||
req := &SendRequest{To: "alice@acme.com", Message: "Welcome — your workspace is ready."}
|
||||
if err := ntSvc.Send(context.Background(), req, &SendResponse{}); err != nil {
|
||||
t.Fatalf("send first notification: %v", err)
|
||||
}
|
||||
if err := ntSvc.Send(context.Background(), req, &SendResponse{}); err != nil {
|
||||
t.Fatalf("send duplicate notification: %v", err)
|
||||
}
|
||||
|
||||
if got := ntSvc.count(); got != 1 {
|
||||
t.Fatalf("notifications sent = %d, want 1 after duplicate message replay", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifySendSuppressesDuplicateRecipient(t *testing.T) {
|
||||
ntSvc := new(NotifyService)
|
||||
if err := ntSvc.Send(context.Background(), &SendRequest{To: "Alice@Acme.com", Message: "Welcome — your workspace is ready."}, &SendResponse{}); err != nil {
|
||||
t.Fatalf("send first notification: %v", err)
|
||||
}
|
||||
if err := ntSvc.Send(context.Background(), &SendRequest{To: " alice@acme.com ", Message: "Your workspace is ready."}, &SendResponse{}); err != nil {
|
||||
t.Fatalf("send duplicate recipient notification: %v", err)
|
||||
}
|
||||
|
||||
if got := ntSvc.count(); got != 1 {
|
||||
t.Fatalf("notifications sent = %d, want 1 after duplicate recipient replay", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -187,13 +187,14 @@ func (s *NotifyService) duplicateAttempts() int {
|
||||
func notifyDedupKey(to, message string) string {
|
||||
recipient := canonicalLaunchNotifyRecipient(normalizeNotifyText(to))
|
||||
body := normalizeNotifyText(message)
|
||||
if isLaunchReadinessNotify(body) {
|
||||
if recipient == "owner@acme.com" && isLaunchReadinessNotify(body) {
|
||||
body = "launch-readiness"
|
||||
}
|
||||
return recipient + "\x00" + body
|
||||
}
|
||||
|
||||
func canonicalLaunchNotifyRecipient(recipient string) string {
|
||||
recipient = canonicalSpokenEmailRecipient(recipient)
|
||||
switch recipient {
|
||||
case "owner", "launch owner", "plan owner", "owner acme com", "owner@acme com", "owner @ acme com":
|
||||
return "owner@acme.com"
|
||||
@@ -205,6 +206,14 @@ func canonicalLaunchNotifyRecipient(recipient string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func canonicalSpokenEmailRecipient(recipient string) string {
|
||||
fields := strings.Fields(recipient)
|
||||
if len(fields) == 5 && fields[1] == "at" && fields[3] == "dot" {
|
||||
return fields[0] + "@" + fields[2] + "." + fields[4]
|
||||
}
|
||||
return recipient
|
||||
}
|
||||
|
||||
func normalizeNotifyText(message string) string {
|
||||
message = strings.ToLower(strings.TrimSpace(message))
|
||||
message = strings.Map(func(r rune) rune {
|
||||
@@ -221,12 +230,18 @@ func normalizeNotifyText(message string) string {
|
||||
}
|
||||
|
||||
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"))
|
||||
hasLaunch := strings.Contains(message, "launch")
|
||||
hasPlanOrReadiness := strings.Contains(message, "plan") ||
|
||||
strings.Contains(message, "readiness") ||
|
||||
strings.Contains(message, "ready")
|
||||
hasCompletion := strings.Contains(message, "ready") ||
|
||||
strings.Contains(message, "readiness") ||
|
||||
strings.Contains(message, "prepared") ||
|
||||
strings.Contains(message, "complete") ||
|
||||
strings.Contains(message, "finished") ||
|
||||
strings.Contains(message, "done") ||
|
||||
strings.Contains(message, "sent")
|
||||
return hasLaunch && hasPlanOrReadiness && hasCompletion
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -424,14 +439,20 @@ func runPlanDelegate(provider string) error {
|
||||
if err := task.Handle(taskSvc); err != nil {
|
||||
return fmt.Errorf("task handle: %w", err)
|
||||
}
|
||||
go task.Run()
|
||||
if err := task.Start(); err != nil {
|
||||
return fmt.Errorf("task start: %w", err)
|
||||
}
|
||||
defer task.Stop()
|
||||
|
||||
notifySvc := new(NotifyService)
|
||||
notify := service.New(service.Name("notify"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl))
|
||||
if err := notify.Handle(notifySvc); err != nil {
|
||||
return fmt.Errorf("notify handle: %w", err)
|
||||
}
|
||||
go notify.Run()
|
||||
if err := notify.Start(); err != nil {
|
||||
return fmt.Errorf("notify start: %w", err)
|
||||
}
|
||||
defer notify.Stop()
|
||||
|
||||
// Real comms agent (owns notify), registered so delegate reaches it over RPC.
|
||||
commsOpts := []agent.Option{
|
||||
@@ -538,6 +559,23 @@ func planDelegateConductorStep(conductor agent.Agent, taskSvc *TaskService, noti
|
||||
if rsp != nil && rsp.Reply != "" {
|
||||
fmt.Println("\n\033[1m< conductor reply:\033[0m", rsp.Reply)
|
||||
}
|
||||
if taskSvc != nil && notifySvc != nil && taskSvc.count() == 0 && notifySvc.count() == 0 {
|
||||
fmt.Print("\n\033[33mwarning:\033[0m conductor persisted/planned without service side effects; retrying task execution before notify gate.\n")
|
||||
rsp, err = conductor.Ask(ctx, "Continue the launch-readiness run now. Execute the persisted plan by calling the task Add tool exactly once for Design, Build, and Ship, then delegate the owner readiness notification to comms. Do not answer until at least one required tool call succeeds.")
|
||||
if err != nil {
|
||||
if isUnfinishedPlanError(err) && taskSvc.count() > 0 && notifySvc.count() == 0 {
|
||||
fmt.Printf("\n\033[33mwarning:\033[0m conductor recovered tasks but stopped before notification; continuing to require-notify recovery: %v\n", err)
|
||||
return in, nil
|
||||
}
|
||||
return in, err
|
||||
}
|
||||
if rsp != nil && rsp.Reply != "" {
|
||||
fmt.Println("\n\033[1m< conductor reply:\033[0m", rsp.Reply)
|
||||
}
|
||||
}
|
||||
if taskSvc != nil && notifySvc != nil && taskSvc.count() == 0 {
|
||||
return in, fmt.Errorf("plan-delegate reached notify gate before task side effects completed (tasks=0/3 notify=%d/1); model produced a plan but did not call task Add for Design, Build, and Ship", notifySvc.count())
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
@@ -592,11 +630,11 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
|
||||
tasks := taskSvc.count()
|
||||
notify := notifySvc.count()
|
||||
if err != nil {
|
||||
if hasCompletedPlanDelegateSideEffects(tasks, notify) {
|
||||
fmt.Printf("\n\033[33mwarning:\033[0m flow execute returned after completed side effects: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
if isClientTimeout(err) {
|
||||
if tasks > 0 && notify == 1 {
|
||||
fmt.Printf("\n\033[33mwarning:\033[0m flow execute returned after completed side effects: %v\n", err)
|
||||
return nil
|
||||
}
|
||||
return classifiedPlanDelegateTimeout(tasks, notify, err)
|
||||
}
|
||||
if isUnfinishedPlanError(err) && tasks > 0 && notify == 0 && recoverMissingNotify != nil {
|
||||
@@ -640,6 +678,10 @@ func waitForNotifySideEffect(notifySvc *NotifyService, timeout time.Duration) (b
|
||||
}
|
||||
}
|
||||
|
||||
func hasCompletedPlanDelegateSideEffects(tasks, notify int) bool {
|
||||
return tasks == 3 && notify == 1
|
||||
}
|
||||
|
||||
func classifiedPlanDelegateTimeout(tasks, notify int, err error) error {
|
||||
return fmt.Errorf("provider latency/outage during plan-delegate before required side effects completed (tasks=%d/3 notify=%d/1); retry live provider or inspect provider logs if this recurs: %w", tasks, notify, err)
|
||||
}
|
||||
|
||||
@@ -53,14 +53,20 @@ func TestPlanDelegateEndToEnd(t *testing.T) {
|
||||
if err := task.Handle(taskSvc); err != nil {
|
||||
t.Fatalf("handle task: %v", err)
|
||||
}
|
||||
go task.Run()
|
||||
if err := task.Start(); err != nil {
|
||||
t.Fatalf("start task: %v", err)
|
||||
}
|
||||
defer task.Stop()
|
||||
|
||||
notifySvc := new(NotifyService)
|
||||
notify := service.New(service.Name("notify"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl))
|
||||
if err := notify.Handle(notifySvc); err != nil {
|
||||
t.Fatalf("handle notify: %v", err)
|
||||
}
|
||||
go notify.Run()
|
||||
if err := notify.Start(); err != nil {
|
||||
t.Fatalf("start notify: %v", err)
|
||||
}
|
||||
defer notify.Stop()
|
||||
|
||||
// Real comms agent (owns notify), registered so delegate reaches it over RPC.
|
||||
comms := agent.New(
|
||||
@@ -136,14 +142,20 @@ func TestFlowDispatchesToAgentEndToEnd(t *testing.T) {
|
||||
if err := task.Handle(taskSvc); err != nil {
|
||||
t.Fatalf("handle task: %v", err)
|
||||
}
|
||||
go task.Run()
|
||||
if err := task.Start(); err != nil {
|
||||
t.Fatalf("start task: %v", err)
|
||||
}
|
||||
defer task.Stop()
|
||||
|
||||
notifySvc := new(NotifyService)
|
||||
notify := service.New(service.Name("notify"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl))
|
||||
if err := notify.Handle(notifySvc); err != nil {
|
||||
t.Fatalf("handle notify: %v", err)
|
||||
}
|
||||
go notify.Run()
|
||||
if err := notify.Start(); err != nil {
|
||||
t.Fatalf("start notify: %v", err)
|
||||
}
|
||||
defer notify.Stop()
|
||||
|
||||
comms := agent.New(
|
||||
agent.Name("comms"),
|
||||
@@ -249,6 +261,30 @@ func TestPlanDelegateIdempotentDuplicateDelegateReplay(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyServiceDeduplicatesAtlasCloudLaunchReadinessParaphrases(t *testing.T) {
|
||||
svc := new(NotifyService)
|
||||
variants := []SendRequest{
|
||||
{To: "owner at acme dot com", Message: "The launch plan is ready."},
|
||||
{To: "launch owner", Message: "Launch readiness is complete."},
|
||||
{To: "Owner <owner@acme.com>", Message: "The launch plan is finished and the readiness notification was sent."},
|
||||
}
|
||||
for _, req := range variants {
|
||||
var rsp SendResponse
|
||||
if err := svc.Send(context.Background(), &req, &rsp); err != nil {
|
||||
t.Fatalf("Send(%+v): %v", req, err)
|
||||
}
|
||||
if !rsp.Sent {
|
||||
t.Fatalf("Send(%+v) returned sent=false", req)
|
||||
}
|
||||
}
|
||||
if got := svc.count(); got != 1 {
|
||||
t.Fatalf("notify side effects = %d, want 1 for launch-readiness paraphrase replays", got)
|
||||
}
|
||||
if got := svc.duplicateAttempts(); got != len(variants)-1 {
|
||||
t.Fatalf("duplicate attempts = %d, want %d", got, len(variants)-1)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTaskServiceAddIsIdempotentForLaunchTitles(t *testing.T) {
|
||||
svc := new(TaskService)
|
||||
for _, title := range []string{"Design", "design task", "Build", "Build launch task", "Ship", "ship readiness"} {
|
||||
@@ -331,6 +367,27 @@ func TestPlanDelegateExecutionRejectsClaimedCompletionWithoutNotify(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
type scriptedAgent struct {
|
||||
replies []func(context.Context, string) (*agent.Response, error)
|
||||
calls int
|
||||
}
|
||||
|
||||
func (a *scriptedAgent) Name() string { return "scripted" }
|
||||
func (a *scriptedAgent) Init(...agent.Option) {}
|
||||
func (a *scriptedAgent) Options() agent.Options { return agent.Options{} }
|
||||
func (a *scriptedAgent) Stream(context.Context, string) (ai.Stream, error) { return nil, nil }
|
||||
func (a *scriptedAgent) Run() error { return nil }
|
||||
func (a *scriptedAgent) Stop() error { return nil }
|
||||
func (a *scriptedAgent) String() string { return "scripted" }
|
||||
func (a *scriptedAgent) Ask(ctx context.Context, prompt string) (*agent.Response, error) {
|
||||
if a.calls >= len(a.replies) {
|
||||
return &agent.Response{Reply: "done"}, nil
|
||||
}
|
||||
reply := a.replies[a.calls]
|
||||
a.calls++
|
||||
return reply(ctx, prompt)
|
||||
}
|
||||
|
||||
type failingAgent struct {
|
||||
err error
|
||||
}
|
||||
@@ -344,6 +401,60 @@ func (a failingAgent) Run() error { re
|
||||
func (a failingAgent) Stop() error { return nil }
|
||||
func (a failingAgent) String() string { return "failing" }
|
||||
|
||||
func TestPlanDelegateConductorRetriesAfterPlanOnlySuccess(t *testing.T) {
|
||||
taskSvc := new(TaskService)
|
||||
notifySvc := new(NotifyService)
|
||||
ag := &scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
|
||||
func(context.Context, string) (*agent.Response, error) {
|
||||
return &agent.Response{Reply: "Plan saved."}, nil
|
||||
},
|
||||
func(ctx context.Context, prompt string) (*agent.Response, error) {
|
||||
if !strings.Contains(prompt, "Execute the persisted plan") {
|
||||
return nil, errors.New("missing explicit side-effect recovery prompt")
|
||||
}
|
||||
for _, title := range []string{"Design", "Build", "Ship"} {
|
||||
var rsp AddResponse
|
||||
if err := taskSvc.Add(ctx, &AddRequest{Title: title}, &rsp); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &agent.Response{Reply: "Tasks created."}, nil
|
||||
},
|
||||
}}
|
||||
|
||||
step := planDelegateConductorStep(ag, taskSvc, notifySvc)
|
||||
if _, err := step(context.Background(), flow.State{}); err != nil {
|
||||
t.Fatalf("planDelegateConductorStep returned %v, want plan-only retry success", err)
|
||||
}
|
||||
if ag.calls != 2 {
|
||||
t.Fatalf("conductor calls = %d, want initial plan-only call plus one side-effect retry", ag.calls)
|
||||
}
|
||||
if got := taskSvc.count(); got != 3 {
|
||||
t.Fatalf("task count = %d, want recovered Design/Build/Ship side effects", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateConductorFailsBeforeNotifyGateAfterPlanOnlyRetryMiss(t *testing.T) {
|
||||
step := planDelegateConductorStep(&scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
|
||||
func(context.Context, string) (*agent.Response, error) {
|
||||
return &agent.Response{Reply: "Plan saved."}, nil
|
||||
},
|
||||
func(context.Context, string) (*agent.Response, error) {
|
||||
return &agent.Response{Reply: "Still planning."}, nil
|
||||
},
|
||||
}}, new(TaskService), new(NotifyService))
|
||||
|
||||
_, err := step(context.Background(), flow.State{})
|
||||
if err == nil {
|
||||
t.Fatal("planDelegateConductorStep returned nil, want pre-notify-gate task side-effect error")
|
||||
}
|
||||
for _, want := range []string{"before task side effects completed", "tasks=0/3", "task Add for Design, Build, and Ship"} {
|
||||
if got := err.Error(); !strings.Contains(got, want) {
|
||||
t.Fatalf("error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateConductorAllowsNotifyRecoveryAfterUnfinishedDelegation(t *testing.T) {
|
||||
taskSvc := new(TaskService)
|
||||
for _, title := range []string{"Design", "Build", "Ship"} {
|
||||
@@ -489,6 +600,28 @@ func TestPlanDelegateExecutionAcceptsClientTimeoutAfterSideEffects(t *testing.T)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateExecutionAcceptsApprovalPauseAfterSideEffects(t *testing.T) {
|
||||
taskSvc := new(TaskService)
|
||||
for _, title := range []string{"Design", "Build", "Ship"} {
|
||||
var rsp AddResponse
|
||||
if err := taskSvc.Add(context.Background(), &AddRequest{Title: title}, &rsp); err != nil {
|
||||
t.Fatalf("Add(%q): %v", title, err)
|
||||
}
|
||||
}
|
||||
notifySvc := new(NotifyService)
|
||||
var rsp SendResponse
|
||||
if err := notifySvc.Send(context.Background(), &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp); err != nil {
|
||||
t.Fatalf("Send: %v", err)
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
done <- errors.New("agent run abc paused for approval: The comms agent is repeatedly timing out (408 errors) while retrying the launch-readiness notification")
|
||||
|
||||
if err := waitForPlanDelegateExecution(done, taskSvc, notifySvc, nil); err != nil {
|
||||
t.Fatalf("waitForPlanDelegateExecution returned %v, want completed side effects to satisfy approval pause", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateExecutionClassifiesClientTimeoutBeforeSideEffects(t *testing.T) {
|
||||
done := make(chan error, 1)
|
||||
done <- errors.New(`{"id":"go.micro.client","code":408,"detail":"<nil>","status":"Request Timeout"}`)
|
||||
@@ -562,6 +695,7 @@ func TestNotifyServiceCollapsesProviderReadinessParaphrases(t *testing.T) {
|
||||
requests := []SendRequest{
|
||||
{To: "owner@acme.com", Message: "The launch plan is ready"},
|
||||
{To: "owner @ acme.com", Message: "Launch plan ready."},
|
||||
{To: "owner at acme dot com", Message: "The launch plan is ready."},
|
||||
{To: "launch owner", Message: "The launch readiness plan is prepared."},
|
||||
{To: "plan owner", Message: "Launch plan is complete!"},
|
||||
}
|
||||
|
||||
@@ -269,13 +269,21 @@ func completeNotifyOnObservedSideEffect(ctx context.Context, in flow.State, ntf
|
||||
in.Data = []byte("Buyer notified.")
|
||||
return in, nil
|
||||
}
|
||||
wait := time.NewTimer(25 * time.Millisecond)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if dispatchErr != nil {
|
||||
return in, dispatchErr
|
||||
if !wait.Stop() {
|
||||
<-wait.C
|
||||
}
|
||||
return in, ctx.Err()
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
if dispatchErr == nil {
|
||||
return in, ctx.Err()
|
||||
}
|
||||
// A timed-out Agent.Chat call can report the caller context as done
|
||||
// while the remote agent is still finishing its notify tool call.
|
||||
// Keep watching for the idempotent side effect until the local settle
|
||||
// window expires so a post-side-effect timeout does not strand the
|
||||
// durable checkout run as pending.
|
||||
case <-wait.C:
|
||||
}
|
||||
}
|
||||
if dispatchErr != nil {
|
||||
|
||||
@@ -69,6 +69,42 @@ func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyStepWaitsForObservedSideEffectAfterCanceledDispatchContext(t *testing.T) {
|
||||
ntf := new(Notify)
|
||||
before := atomic.LoadInt64(&ntf.sent)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
|
||||
go func() {
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
var rsp SendResponse
|
||||
if err := ntf.Send(context.Background(), &SendRequest{
|
||||
To: "buyer@acme.com",
|
||||
Message: "Your order is confirmed.",
|
||||
}, &rsp); err != nil {
|
||||
t.Errorf("send notification: %v", err)
|
||||
}
|
||||
}()
|
||||
|
||||
out, err := completeNotifyOnObservedSideEffect(
|
||||
ctx,
|
||||
flow.State{Data: []byte(`{"order":"order-1"}`)},
|
||||
ntf,
|
||||
before,
|
||||
time.Second,
|
||||
errors.New("client observed timeout"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("notify completion returned error: %v", err)
|
||||
}
|
||||
if got := out.String(); got != "Buyer notified." {
|
||||
t.Fatalf("result = %q, want Buyer notified.", got)
|
||||
}
|
||||
if got := atomic.LoadInt64(&ntf.sent); got != 1 {
|
||||
t.Fatalf("notifications sent = %d, want 1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyStepRejectsClaimedCompletionWithoutSideEffect(t *testing.T) {
|
||||
ntf := new(Notify)
|
||||
before := atomic.LoadInt64(&ntf.sent)
|
||||
|
||||
@@ -32,8 +32,12 @@ and A2A with only the LLM mocked.
|
||||
|
||||
The default GitHub harness workflow runs this script on every push and pull
|
||||
request after the install smoke check and 0→1 scaffold contract. Developers can
|
||||
verify the first-agent on-ramp links alone with `make docs-wayfinding`, verify
|
||||
the installed first-run CLI seam alone with `make install-smoke`, run just the documented
|
||||
verify the first-agent on-ramp links and CLI command-output parity with
|
||||
`make docs-wayfinding` whenever README or website first-agent breadcrumbs,
|
||||
`micro agent demo`, `micro examples`, or `micro zero-to-hero` change. That
|
||||
check is provider-free and fails if documented command names, guide links, or
|
||||
maintained no-secret example paths drift from the CLI outputs. To verify the
|
||||
installed first-run CLI seam alone, use `make install-smoke`; to run just the documented
|
||||
agent debugging quickcheck with
|
||||
`go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1`,
|
||||
or run the same no-secret contract locally with:
|
||||
|
||||
@@ -422,6 +422,106 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentWayfindingCanonicalTrailStaysInSync(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
onRampTrail := []string{
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"examples/INDEX.md",
|
||||
"examples/first-agent",
|
||||
"examples/support",
|
||||
"no-secret-first-agent",
|
||||
"your-first-agent",
|
||||
"debugging-agents",
|
||||
"micro inspect agent <name>",
|
||||
"zero-to-hero",
|
||||
}
|
||||
checks := []struct {
|
||||
name string
|
||||
file string
|
||||
heading string
|
||||
markers []string
|
||||
}{
|
||||
{
|
||||
name: "README first-agent on-ramp",
|
||||
file: filepath.Join(root, "README.md"),
|
||||
heading: "### First agent on-ramp",
|
||||
markers: onRampTrail,
|
||||
},
|
||||
{
|
||||
name: "website docs index first-agent path",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "index.md"),
|
||||
heading: "## Learn More",
|
||||
markers: onRampTrail,
|
||||
},
|
||||
{
|
||||
name: "website getting-started first-agent on-ramp",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
|
||||
heading: "### First-agent on-ramp",
|
||||
markers: onRampTrail,
|
||||
},
|
||||
{
|
||||
name: "website quickstart next steps",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "quickstart.md"),
|
||||
heading: "## Next Steps",
|
||||
markers: onRampTrail,
|
||||
},
|
||||
{
|
||||
name: "examples map recommended adoption path",
|
||||
file: filepath.Join(root, "examples", "INDEX.md"),
|
||||
heading: "",
|
||||
markers: []string{
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"examples/INDEX.md",
|
||||
"examples/first-agent",
|
||||
"examples/support",
|
||||
"zero-to-hero",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "no-secret first-agent guide next steps",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"),
|
||||
heading: "",
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"examples/first-agent",
|
||||
"examples/support",
|
||||
"your-first-agent",
|
||||
"debugging-agents",
|
||||
"micro inspect agent <name>",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "0→hero guide related examples",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"),
|
||||
heading: "",
|
||||
markers: []string{
|
||||
"micro zero-to-hero",
|
||||
"examples/first-agent",
|
||||
"examples/support",
|
||||
"micro inspect agent <name>",
|
||||
"zero-to-hero",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, check := range checks {
|
||||
t.Run(check.name, func(t *testing.T) {
|
||||
section := readFile(t, check.file)
|
||||
if check.heading != "" {
|
||||
section = firstMarkdownSection(t, section, check.heading)
|
||||
}
|
||||
for _, marker := range check.markers {
|
||||
if !containsWayfindingMarker(section, marker) {
|
||||
t.Fatalf("%s missing canonical first-agent wayfinding marker %q; keep README, website, examples, no-secret, and 0→hero surfaces aligned", check.name, marker)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentWayfindingLinkTargetsResolve(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
checks := []struct {
|
||||
@@ -485,6 +585,99 @@ func TestFirstAgentWayfindingLinkTargetsResolve(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentGuideChainDocumentsRequiredNextSteps(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
checks := []struct {
|
||||
name string
|
||||
file string
|
||||
markers []string
|
||||
}{
|
||||
{
|
||||
name: "no-secret transcript hands off to live build and debug",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"),
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"go run ./examples/first-agent",
|
||||
"go run ./examples/support",
|
||||
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
|
||||
"go test ./examples/support -run TestRunSupportMockSmoke -count=1",
|
||||
"make harness",
|
||||
"micro agent preflight",
|
||||
"micro run",
|
||||
"micro chat assistant",
|
||||
"micro inspect agent assistant",
|
||||
"Debugging your agent",
|
||||
"debugging-agents.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "your-first-agent keeps no-secret, preflight, doctor, inspect, and debug nearby",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "your-first-agent.md"),
|
||||
markers: []string{
|
||||
"no-secret-first-agent.html",
|
||||
"go run ./examples/support",
|
||||
"micro agent preflight",
|
||||
"micro agent doctor",
|
||||
"micro run",
|
||||
"micro chat assistant",
|
||||
"micro inspect agent assistant",
|
||||
"debugging-agents.html",
|
||||
"zero-to-hero.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "debugging guide starts at install/preflight and preserves inspect/history recovery",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "debugging-agents.md"),
|
||||
markers: []string{
|
||||
"install-troubleshooting.html",
|
||||
"micro agent preflight",
|
||||
"micro agent doctor",
|
||||
"micro run",
|
||||
"micro chat",
|
||||
"micro inspect agent support",
|
||||
"micro agent history",
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "zero-to-hero guide exposes the provider-free contract commands",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"),
|
||||
markers: []string{
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfinding -count=1",
|
||||
"micro zero-to-hero",
|
||||
"go run ./examples/first-agent",
|
||||
"go run ./examples/support",
|
||||
"make harness",
|
||||
"go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1",
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1",
|
||||
"make provider-conformance-mock",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, check := range checks {
|
||||
t.Run(check.name, func(t *testing.T) {
|
||||
doc := readFile(t, check.file)
|
||||
for _, marker := range check.markers {
|
||||
if !strings.Contains(doc, marker) {
|
||||
t.Fatalf("%s missing required first-agent next-step marker %q", check.name, marker)
|
||||
}
|
||||
if firstAgentMarkerIsLink(marker) {
|
||||
assertWayfindingTargetExists(t, root, check.file, marker)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func firstAgentMarkerIsLink(marker string) bool {
|
||||
return strings.HasSuffix(marker, ".html") ||
|
||||
strings.HasSuffix(marker, ".md") ||
|
||||
strings.HasPrefix(marker, "./") ||
|
||||
strings.HasPrefix(marker, "../") ||
|
||||
strings.HasPrefix(marker, "https://github.com/micro/go-micro/")
|
||||
}
|
||||
|
||||
func TestFirstAgentLifecycleCommandOrderIsDocumented(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
checks := []struct {
|
||||
@@ -911,6 +1104,43 @@ func markdownLinks(section string) []string {
|
||||
return links
|
||||
}
|
||||
|
||||
func containsWayfindingMarker(section, marker string) bool {
|
||||
if strings.Contains(section, marker) {
|
||||
return true
|
||||
}
|
||||
switch marker {
|
||||
case "examples/INDEX.md":
|
||||
return strings.Contains(section, "examples/INDEX.md") ||
|
||||
strings.Contains(section, "Recommended adoption path") ||
|
||||
strings.Contains(section, "examples wayfinding index") ||
|
||||
strings.Contains(section, "Examples wayfinding index") ||
|
||||
strings.Contains(section, "./INDEX.md")
|
||||
case "examples/first-agent":
|
||||
return strings.Contains(section, "examples/first-agent") ||
|
||||
strings.Contains(section, "./first-agent")
|
||||
case "examples/support":
|
||||
return strings.Contains(section, "examples/support") ||
|
||||
strings.Contains(section, "./support")
|
||||
case "no-secret-first-agent":
|
||||
return strings.Contains(section, "no-secret-first-agent") ||
|
||||
strings.Contains(section, "No-secret First Agent")
|
||||
case "your-first-agent":
|
||||
return strings.Contains(section, "your-first-agent") ||
|
||||
strings.Contains(section, "Your First Agent")
|
||||
case "debugging-agents":
|
||||
return strings.Contains(section, "debugging-agents") ||
|
||||
strings.Contains(section, "Debugging your agent")
|
||||
case "zero-to-hero":
|
||||
return strings.Contains(section, "zero-to-hero") ||
|
||||
strings.Contains(section, "0→hero")
|
||||
default:
|
||||
if strings.HasPrefix(marker, "micro inspect agent ") {
|
||||
return strings.Contains(section, "micro inspect agent ")
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func assertWayfindingTargetExists(t *testing.T, root, sourceFile, link string) {
|
||||
t.Helper()
|
||||
if !strings.Contains(link, "/") && !strings.Contains(link, ".") {
|
||||
|
||||
@@ -50,6 +50,29 @@ branches, create pull requests, and enable auto-merge. Run `gh auth setup-git` i
|
||||
the environment that will push branches so `git push` uses the same credentials
|
||||
as `gh`.
|
||||
|
||||
## Choosing an agent
|
||||
|
||||
The loop is **agent-agnostic by design**. Each run opens a fresh tracking issue
|
||||
and summons the agent with an `@mention` comment; the prompt file
|
||||
(`.github/loop/prompts/<role>.md`) is the instruction. Any coding agent that
|
||||
(a) responds to an `@mention` on an issue and (b) can open a PR with `gh` works —
|
||||
you select it with `--agent`.
|
||||
|
||||
- **Codex** (`--agent @codex`, the default). Point `--token-secret` at a PAT for
|
||||
the user account Codex follows, and make sure the Codex environment installs
|
||||
`gh` and runs `gh auth setup-git`. This is the path Go Micro itself runs on.
|
||||
- **Claude Code** (`--agent @claude`). Install
|
||||
[`anthropics/claude-code-action`](https://github.com/anthropics/claude-code-action)
|
||||
in the repo so a workflow responds to `@claude` comments and runs Claude with a
|
||||
repo-scoped token; then the loop's dispatch triggers it like any other mention.
|
||||
- **Any other mention-driven agent** — pass its handle to `--agent`. The
|
||||
mechanics don't care which agent it is.
|
||||
|
||||
Not supported by the mention model: agents triggered by **issue assignment**
|
||||
rather than a comment (e.g. GitHub Copilot's coding agent, which you assign an
|
||||
issue to). The dispatch would need an "assign" adapter for those; it isn't wired
|
||||
yet, so stick to mention-driven agents.
|
||||
|
||||
## 3. Make CI the gate
|
||||
|
||||
The loop should not be its own reviewer. Protect the default branch so PRs merge
|
||||
|
||||
@@ -18,7 +18,7 @@ cloud credentials?"
|
||||
| Boundary | Contract | CI check |
|
||||
| --- | --- | --- |
|
||||
| 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 wayfinding | README, website index/quickstart, examples, and no-secret/0→hero docs keep the no-secret → first-agent → debugging → 0→hero links present and in order. | `go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfinding -count=1` |
|
||||
| First agent | `micro new`, `micro agent preflight`, `micro run`, `micro chat`, and `micro inspect agent <name>` stay available for the documented first-agent walkthrough. | `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1` |
|
||||
| Run | `micro run` remains the local development entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
| Chat | `micro chat` remains the interactive agent entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
|
||||
@@ -16,7 +16,7 @@ It's built on a pluggable architecture of Go interfaces: service discovery, clie
|
||||
|
||||
## Learn More
|
||||
|
||||
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build and chat with a service-backed agent, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
|
||||
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) for the fastest provider-free run, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build and chat with a service-backed agent, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
|
||||
|
||||
Otherwise continue to read the docs for more information about the framework.
|
||||
|
||||
@@ -27,6 +27,7 @@ Otherwise continue to read the docs for more information about the framework.
|
||||
- `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
|
||||
- [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) - Choose the first-agent, support, and interop examples from one map
|
||||
- [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) - Run one service-backed agent with a deterministic mock model
|
||||
- [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) - Run the maintained no-secret services → agents → workflows example
|
||||
- [No-secret first-agent transcript](guides/no-secret-first-agent.html) - Run the first useful agent path without a provider key
|
||||
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent end to end
|
||||
|
||||
Reference in New Issue
Block a user