Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 926012afaa |
@@ -21,8 +21,9 @@ changes, architectural rewrites. Those go to the human.
|
||||
|
||||
## Work queue (ranked)
|
||||
|
||||
1. **Verify first-agent docs commands in CI** ([#4696](https://github.com/micro/go-micro/issues/4696)) — With #4702 closing the broad provider retry-cancellation seam, the highest-value Now-roadmap work returns to developer adoption: make the README/website first-agent path a focused no-secret command contract. The mission is to make services → agents → workflows feel like one runtime, and the current risk is not lack of depth but an on-ramp that can drift from runnable behavior. Keep this scoped to verified commands, example wayfinding, and short mock-model checks; do not redesign public APIs.
|
||||
2. **Harden AtlasCloud plan-delegate tool-call recovery** ([#4699](https://github.com/micro/go-micro/issues/4699)) — Recent provider resilience work shipped retry cancellation and failure metadata, but the live AtlasCloud plan/delegate harness exposed a concrete malformed/nested tool-call recovery gap. Fixing it protects the agent loop's operability across providers while keeping the queue honest about current CI signals. Keep this to recovery/rejection behavior and regression coverage; surface any provider-contract or default-behavior changes for human review.
|
||||
1. **Ensure AtlasCloud plan-delegate executes required side effects** ([#4546](https://github.com/micro/go-micro/issues/4546)) — The live provider conformance flow can stop after persisting a plan, then reach the notification gate with no Design/Build/Ship task records and no delegated launch-readiness notification. This is the highest-value open gap because plan/delegate is the core services → agents → workflows promise: an agent must turn a plan into durable service side effects, not just describe work.
|
||||
2. **Ensure AtlasCloud agent-flow sends onboarding notification** ([#4529](https://github.com/micro/go-micro/issues/4529)) — The event-driven onboarding flow can create the workspace but miss the required notification before timing out. This remains a direct services → agents → workflows seam and a developer-trust issue for 0→hero because the workflow appears partly successful while a required side effect is absent.
|
||||
3. **Make AtlasCloud universe A2A reachability probe deterministic** ([#4504](https://github.com/micro/go-micro/issues/4504)) — The universe checkout flow completes, but the A2A reachability probe can time out under AtlasCloud. This still matters for cross-framework operability and agent discoverability, but ranks after side-effect execution gaps because it is currently isolated to post-flow reachability rather than lost business effects.
|
||||
|
||||
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
|
||||
architecture-review pass._
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
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.25"
|
||||
go-version: 1.24
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: golangci-lint
|
||||
|
||||
@@ -8,7 +8,7 @@ name: "Loop: Triage"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Harness (E2E)", "Lint", "Run Tests", "govulncheck"]
|
||||
workflows: ["Harness (E2E)", "Lint", "Run Tests"]
|
||||
types: [completed]
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -19,7 +19,7 @@ jobs:
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v3
|
||||
with:
|
||||
go-version: "1.25"
|
||||
go-version: 1.24
|
||||
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.25"
|
||||
go-version: 1.24
|
||||
check-latest: true
|
||||
cache: true
|
||||
- name: Get dependencies
|
||||
|
||||
@@ -17,33 +17,8 @@ below is kept current between tags and rolled into the next version when it ship
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- **First-agent chat/inspect fixture** — the maintained first-agent CLI fixture now covers chat and inspect boundaries together. (`internal/harness/`, `cmd/micro/`)
|
||||
- **Zero-to-hero inspect transcript check** — the 0→hero harness now verifies the inspect transcript path stays visible in the lifecycle walkthrough. (`internal/harness/zero-to-hero-ci/`, `internal/website/docs/`)
|
||||
|
||||
### Changed
|
||||
- **Plan-delegate plan persistence** — plan/delegate runs now persist plan state more defensively across harness scenarios. (`agent/`, `internal/harness/`)
|
||||
|
||||
### Fixed
|
||||
- **Zero-to-hero fixture output race** — 0→hero fixture output is less race-prone during harness runs. (`internal/harness/zero-to-hero-ci/`)
|
||||
|
||||
---
|
||||
|
||||
## [6.6.0] - July 2026
|
||||
|
||||
### Added
|
||||
- **First-agent guide chain contract** — the harness now verifies the install → demo → examples → 0→hero guide chain stays connected for new agent builders. (`internal/harness/`, `internal/website/docs/`)
|
||||
- **First-agent docs wayfinding guard** — the local harness now includes a focused no-network check for first-agent and 0→hero docs links. (`Makefile`, `internal/harness/`)
|
||||
- **First-agent quickcheck breadcrumbs** — first-agent docs now surface quickcheck wayfinding for install, scaffold, chat, inspect, and recovery paths. (`internal/website/docs/`, `README.md`)
|
||||
- **First-agent chat wayfinding verification** — the harness now verifies first-agent chat wayfinding remains discoverable from the public docs route. (`internal/harness/`, `internal/website/docs/`)
|
||||
|
||||
### Changed
|
||||
- **Universe A2A reachability probe** — the universe harness now exercises A2A reachability more defensively. (`internal/harness/`)
|
||||
- **AtlasCloud workspace repair fallback** — AtlasCloud fallback handling now recovers workspace-repair tool calls more reliably. (`ai/atlascloud/`, `agent/`)
|
||||
- **AtlasCloud empty-argument tool repair** — AtlasCloud text tool-call repair now handles empty-argument calls more consistently. (`ai/atlascloud/`, `agent/`)
|
||||
|
||||
### Removed
|
||||
- **`go-micro.dev/v6/ai/flow`** — the alias-only backward-compatibility shim is removed; import the canonical [`go-micro.dev/v6/flow`](flow) instead (same types and functions). It had no internal callers. (`ai/flow/`)
|
||||
|
||||
### Fixed
|
||||
- **A2A fallback artifact text** — A2A fallback responses now avoid leaking provider artifact text into agent-visible output. (`gateway/a2a/`, `agent/`)
|
||||
@@ -51,20 +26,6 @@ below is kept current between tags and rolled into the next version when it ship
|
||||
- **Plan-delegate harness cleanup** — plan/delegate harness cleanup is more reliable after conformance runs. (`internal/harness/`)
|
||||
- **AtlasCloud spoken notify replays** — AtlasCloud fallback handling now collapses spoken notification replays more consistently. (`ai/atlascloud/`, `agent/`)
|
||||
- **Agent-flow onboarding side effects** — onboarding side-effect checks are more stable across the agent-flow harness. (`agent/`, `internal/harness/`)
|
||||
- **Plan-delegate plan-only side effects** — plan/delegate recovery now preserves plan-only side effects more reliably. (`agent/`, `internal/harness/`)
|
||||
- **Checkpointed tool result recording** — checkpoint resume paths now guard tool-result recording against duplicate or stale writes. (`agent/`)
|
||||
- **Agent timeout notification completion** — universe runs now finalize observed notifications more reliably after agent timeouts. (`agent/`, `internal/harness/`)
|
||||
- **Completed plan-delegate side effects** — completed plan/delegate side effects are accepted more consistently in recovery paths. (`agent/`, `internal/harness/`)
|
||||
- **Agent-flow onboarding notifications** — agent-flow onboarding notification recovery is more reliable across replay scenarios. (`agent/`, `internal/harness/`)
|
||||
|
||||
### Documentation
|
||||
- **Agent-agnostic mention model** — loop docs now describe the mention-driven agent model without binding it to one coding agent. (`internal/docs/`, `.github/loop/`)
|
||||
- **First-agent quickcheck docs** — public docs now surface the first-agent quickcheck path for faster troubleshooting. (`internal/website/docs/`)
|
||||
- **Agent resume breadcrumbs** — docs now add clearer resume breadcrumbs for checkpointed agent runs. (`internal/website/docs/`)
|
||||
|
||||
### Security
|
||||
- **Govulncheck vulnerability gate** — CI now includes a govulncheck gate and wires vulnerability failures into loop triage. (`.github/workflows/`, `cmd/micro/loop/`)
|
||||
- **Dependency vulnerability patches** — toolchain and dependency updates patch reachable CVEs across the project. (`go.mod`, `go.sum`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ 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 'TestFirstAgentWayfinding' -count=1
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestFirstAgentWayfindingDocs|TestFirstAgentWayfindingLinkTargetsResolve' -count=1
|
||||
go test ./cmd/micro -run 'TestFirstAgentDocsMatchCLIOutput|TestFirstAgentWalkthroughCLIBoundaries' -count=1
|
||||
|
||||
# Verify the documented install script and first-run CLI command boundaries without
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# Go Micro [](https://pkg.go.dev/go-micro.dev/v6?tab=doc) [](https://discord.gg/G8Gk5j3uXr)
|
||||
# Go Micro [](https://pkg.go.dev/go-micro.dev/v6?tab=doc) [](https://goreportcard.com/report/github.com/go-micro/go-micro) [](https://discord.gg/G8Gk5j3uXr)
|
||||
|
||||
Go Micro is an **agent harness** and service framework for Go.
|
||||
|
||||
@@ -97,24 +97,19 @@ After install and the first `micro new`/`micro run` smoke check, take the
|
||||
walkable agent path in this order:
|
||||
|
||||
1. [Install troubleshooting](internal/website/docs/guides/install-troubleshooting.md) — verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path before agent work.
|
||||
|
||||
Run `make docs-wayfinding` to verify the focused no-secret docs/CLI contract that keeps these README and website commands aligned with the installed CLI.
|
||||
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
|
||||
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
6. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
|
||||
7. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
|
||||
8. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
|
||||
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
5. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
|
||||
6. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
|
||||
7. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
|
||||
maintained support agent with a mock model and see services → agents → workflows succeed without a key.
|
||||
9. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
8. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
service-backed agent and talk to it with `micro chat`.
|
||||
10. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
|
||||
`micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`,
|
||||
then `micro chat` and `micro inspect agent <name>` to recover run history, memory,
|
||||
and provider checks when the first conversation does something unexpected.
|
||||
11. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
|
||||
9. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
|
||||
`micro inspect agent <name>`, run history, memory, and provider checks when the first
|
||||
conversation does something unexpected.
|
||||
10. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
|
||||
services → agents → workflows loop with scaffold, run, chat, inspect, flow
|
||||
history, and deploy dry-run commands that match the maintained harness.
|
||||
|
||||
|
||||
@@ -355,8 +355,6 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
|
||||
})
|
||||
if err != nil {
|
||||
run.Status = agentRunFailureStatus(err)
|
||||
failureKind := ai.ClassifyError(err)
|
||||
attempts := agentRunFailureAttempts(err)
|
||||
err = agentOperationalError(err)
|
||||
if a.currentRun != nil {
|
||||
run.Steps = a.currentRun.Steps
|
||||
@@ -365,9 +363,7 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
|
||||
run.Steps = []flow.StepRecord{{Name: agentAskStep}}
|
||||
}
|
||||
run.Steps[0].Status = run.Status
|
||||
run.Steps[0].Attempts = attempts
|
||||
run.Steps[0].Error = err.Error()
|
||||
run.Steps[0].ErrorKind = string(failureKind)
|
||||
_ = a.saveRun(ctx, run)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -296,9 +296,6 @@ func (a *agentImpl) planWrap(next ai.ToolHandler) ai.ToolHandler {
|
||||
if call.Name == toolPlan {
|
||||
return a.handlePlan(call)
|
||||
}
|
||||
if containsNestedTextToolCall(call.Input) {
|
||||
return refused(call.ID, ai.RefusedApproval, "malformed tool call: nested text tool-call markup found inside arguments; call the intended tool directly with clean JSON arguments")
|
||||
}
|
||||
if call.Name == toolDelegate {
|
||||
if blocked := a.unfinishedPlanStepsBeforeDelegation(); len(blocked) > 0 {
|
||||
return refused(call.ID, ai.RefusedApproval, "complete these plan steps before delegating: "+strings.Join(blocked, ", "))
|
||||
|
||||
+12
-25
@@ -3,7 +3,6 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -216,14 +215,6 @@ func (e *operationalError) Unwrap() error {
|
||||
return e.err
|
||||
}
|
||||
|
||||
func agentRunFailureAttempts(err error) int {
|
||||
var retryErr *ai.RetryError
|
||||
if err != nil && errors.As(err, &retryErr) && retryErr.Attempts > 0 {
|
||||
return retryErr.Attempts
|
||||
}
|
||||
return 1
|
||||
}
|
||||
|
||||
func agentOperationalError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
@@ -244,32 +235,28 @@ func agentOperationalError(err error) error {
|
||||
|
||||
func (a *agentImpl) checkpointToolWrap(next ai.ToolHandler) ai.ToolHandler {
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
run := a.currentRun
|
||||
if a.opts.Checkpoint == nil || run == nil {
|
||||
if a.opts.Checkpoint == nil || a.currentRun == nil {
|
||||
return next(ctx, call)
|
||||
}
|
||||
name := toolCheckpointName(call)
|
||||
if rec, ok := findStep(run.Steps, name); ok && rec.Status == "done" {
|
||||
if rec, ok := findStep(a.currentRun.Steps, name); ok && rec.Status == "done" {
|
||||
return ai.ToolResult{ID: call.ID, Value: rec.Result, Content: rec.Result}
|
||||
}
|
||||
|
||||
idx := upsertStep(&run.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
|
||||
_ = a.saveRun(ctx, *run)
|
||||
idx := upsertStep(&a.currentRun.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
|
||||
_ = a.saveRun(ctx, *a.currentRun)
|
||||
res := next(ctx, call)
|
||||
if idx < 0 || idx >= len(run.Steps) || run.Steps[idx].Name != name {
|
||||
idx = upsertStep(&run.Steps, flow.StepRecord{Name: name, Status: "in_progress"})
|
||||
}
|
||||
run.Steps[idx].Attempts++
|
||||
a.currentRun.Steps[idx].Attempts++
|
||||
if res.Refused != "" {
|
||||
run.Steps[idx].Status = "failed"
|
||||
run.Steps[idx].Error = res.Content
|
||||
_ = a.saveRun(ctx, *run)
|
||||
a.currentRun.Steps[idx].Status = "failed"
|
||||
a.currentRun.Steps[idx].Error = res.Content
|
||||
_ = a.saveRun(ctx, *a.currentRun)
|
||||
return res
|
||||
}
|
||||
run.Steps[idx].Status = "done"
|
||||
run.Steps[idx].Result = res.Content
|
||||
run.Steps[idx].Error = ""
|
||||
_ = a.saveRun(ctx, *run)
|
||||
a.currentRun.Steps[idx].Status = "done"
|
||||
a.currentRun.Steps[idx].Result = res.Content
|
||||
a.currentRun.Steps[idx].Error = ""
|
||||
_ = a.saveRun(ctx, *a.currentRun)
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
@@ -154,45 +154,6 @@ func TestCheckpointSkipsDuplicateToolWithinAsk(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointToolWrapSurvivesClearedCurrentRun(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "tool-cleared-run-agent")
|
||||
run := flow.Run{
|
||||
ID: "run-1",
|
||||
Flow: "tool-cleared-run-agent",
|
||||
Status: "running",
|
||||
Steps: []flow.StepRecord{{Name: agentAskStep, Status: "in_progress"}},
|
||||
}
|
||||
a := &agentImpl{
|
||||
opts: newOptions(Name("tool-cleared-run-agent"), WithCheckpoint(cp)),
|
||||
currentRun: &run,
|
||||
}
|
||||
|
||||
handler := a.checkpointToolWrap(func(context.Context, ai.ToolCall) ai.ToolResult {
|
||||
a.currentRun = nil
|
||||
return ai.ToolResult{ID: "call-1", Content: "created"}
|
||||
})
|
||||
res := handler(ctx, ai.ToolCall{ID: "call-1", Name: "external.create", Input: map[string]any{"title": "Design"}})
|
||||
if res.Content != "created" {
|
||||
t.Fatalf("tool result = %q, want created", res.Content)
|
||||
}
|
||||
|
||||
loaded, ok, err := cp.Load(ctx, "run-1")
|
||||
if err != nil {
|
||||
t.Fatalf("load checkpoint: %v", err)
|
||||
}
|
||||
if !ok {
|
||||
t.Fatal("checkpoint missing")
|
||||
}
|
||||
rec, ok := findStep(loaded.Steps, `tool:external.create:{"title":"Design"}`)
|
||||
if !ok {
|
||||
t.Fatalf("checkpoint steps = %#v, want completed tool step", loaded.Steps)
|
||||
}
|
||||
if rec.Status != "done" || rec.Result != "created" || rec.Attempts != 1 {
|
||||
t.Fatalf("tool checkpoint = %#v, want done result with one attempt", rec)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckpointContinuesRunWithUnfinishedPlanStep(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cp := flow.StoreCheckpoint(store.NewMemoryStore(), "unfinished-plan-agent")
|
||||
|
||||
+20
-54
@@ -22,20 +22,17 @@ 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 := agentConformanceProviders
|
||||
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},
|
||||
}
|
||||
|
||||
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
|
||||
for _, provider := range providers {
|
||||
@@ -50,7 +47,16 @@ func TestAgentProviderConformanceMatrix(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
|
||||
providers := streamConformanceProviders()
|
||||
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},
|
||||
}
|
||||
|
||||
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
|
||||
for _, provider := range providers {
|
||||
@@ -64,46 +70,6 @@ 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 {
|
||||
|
||||
@@ -90,23 +90,3 @@ func TestApproveToolDoesNotGatePlan(t *testing.T) {
|
||||
t.Error("plan should have been persisted despite the denying approver")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNestedTextToolCallArgumentsAreRefused(t *testing.T) {
|
||||
called := false
|
||||
a := newTestAgent(Name("nested-tool-arg"),
|
||||
WithTool("task.add", "add task", nil, func(context.Context, map[string]any) (string, error) {
|
||||
called = true
|
||||
return "created", nil
|
||||
}),
|
||||
)
|
||||
|
||||
content := toolContent(a.toolHandler(), "task.add", map[string]any{
|
||||
"title": `Continue the launch plan. <tool_call name="plan">{"steps":[{"task":"Design","status":"pending"}]}</tool_call>`,
|
||||
})
|
||||
if called {
|
||||
t.Fatal("tool handler ran despite nested text tool-call markup in arguments")
|
||||
}
|
||||
if !strings.Contains(content, "nested text tool-call markup") {
|
||||
t.Fatalf("content = %q, want nested tool-call refusal", content)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -327,12 +327,6 @@ func TestAskCheckpointRecordsTerminalOperationalFailureStatus(t *testing.T) {
|
||||
if len(runs[0].Steps) == 0 || runs[0].Steps[0].Status != tt.want {
|
||||
t.Fatalf("step status = %#v, want %q", runs[0].Steps, tt.want)
|
||||
}
|
||||
if runs[0].Steps[0].Attempts != 1 {
|
||||
t.Fatalf("step attempts = %d, want 1", runs[0].Steps[0].Attempts)
|
||||
}
|
||||
if got := runs[0].Steps[0].ErrorKind; got != string(ai.ClassifyError(tt.err)) {
|
||||
t.Fatalf("step error kind = %q, want %q", got, ai.ClassifyError(tt.err))
|
||||
}
|
||||
if pending, err := Pending(context.Background(), a); err != nil || len(pending) != 0 {
|
||||
t.Fatalf("Pending = %#v, %v; want no terminal run", pending, err)
|
||||
}
|
||||
|
||||
@@ -263,27 +263,6 @@ func textToolArguments(raw any) map[string]any {
|
||||
return nil
|
||||
}
|
||||
|
||||
func containsNestedTextToolCall(v any) bool {
|
||||
switch x := v.(type) {
|
||||
case string:
|
||||
text := html.UnescapeString(x)
|
||||
return openingTaggedToolCall.MatchString(text) || singleTaggedToolCall.MatchString(text)
|
||||
case map[string]any:
|
||||
for _, item := range x {
|
||||
if containsNestedTextToolCall(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
case []any:
|
||||
for _, item := range x {
|
||||
if containsNestedTextToolCall(item) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func decodeTaggedTextToolCalls(text string, allowed map[string]string) []ai.ToolCall {
|
||||
var out []ai.ToolCall
|
||||
for _, match := range singleTaggedToolCall.FindAllStringSubmatch(text, -1) {
|
||||
|
||||
@@ -554,104 +554,8 @@ func atlascloudFallbackTextToolCall(toolName string, req *ai.Request) string {
|
||||
case "delegate":
|
||||
return atlascloudDelegateFallbackTextToolCall(req)
|
||||
default:
|
||||
if atlascloudToolTakesNoArguments(toolName, req.Tools) {
|
||||
return atlascloudEmptyArgumentFallbackTextToolCall(toolName)
|
||||
}
|
||||
return atlascloudServiceFallbackTextToolCall(toolName, req)
|
||||
}
|
||||
}
|
||||
|
||||
func atlascloudServiceFallbackTextToolCall(toolName string, req *ai.Request) string {
|
||||
if req == nil {
|
||||
return ""
|
||||
}
|
||||
for _, tool := range req.Tools {
|
||||
if tool.Name != toolName {
|
||||
continue
|
||||
}
|
||||
args := atlascloudFallbackArgsForProperties(tool.Properties, atlascloudRequestText(req))
|
||||
if args == nil {
|
||||
return ""
|
||||
}
|
||||
b, err := json.Marshal(args)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return `<tool_call name="` + toolName + `">` + string(b) + `</tool_call>`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func atlascloudFallbackArgsForProperties(properties map[string]any, ctxText string) map[string]any {
|
||||
if len(properties) == 0 {
|
||||
return map[string]any{}
|
||||
}
|
||||
args := make(map[string]any, len(properties))
|
||||
for name, schema := range properties {
|
||||
value, ok := atlascloudFallbackArgValue(name, schema, ctxText)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
args[name] = value
|
||||
}
|
||||
return args
|
||||
}
|
||||
|
||||
func atlascloudFallbackArgValue(name string, schema any, ctxText string) (any, bool) {
|
||||
typeName := "string"
|
||||
if m, ok := schema.(map[string]any); ok {
|
||||
if t, _ := m["type"].(string); t != "" {
|
||||
typeName = t
|
||||
}
|
||||
}
|
||||
switch typeName {
|
||||
case "string":
|
||||
return atlascloudFallbackStringArg(name, ctxText)
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
|
||||
func atlascloudFallbackStringArg(name, ctxText string) (string, bool) {
|
||||
ctxText = strings.TrimSpace(ctxText)
|
||||
if ctxText == "" {
|
||||
return "", false
|
||||
}
|
||||
if strings.Contains(strings.ToLower(name), "email") || strings.Contains(strings.ToLower(name), "owner") {
|
||||
if email := atlascloudFirstEmail(ctxText); email != "" {
|
||||
return email, true
|
||||
}
|
||||
}
|
||||
return ctxText, true
|
||||
}
|
||||
|
||||
func atlascloudFirstEmail(text string) string {
|
||||
for _, field := range strings.FieldsFunc(text, func(r rune) bool {
|
||||
return strings.ContainsRune(" \t\n\r<>\"'(),;", r)
|
||||
}) {
|
||||
field = strings.Trim(field, ".:")
|
||||
if strings.Contains(field, "@") && strings.Contains(field, ".") {
|
||||
return field
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func atlascloudToolTakesNoArguments(toolName string, tools []ai.Tool) bool {
|
||||
for _, tool := range tools {
|
||||
if tool.Name != toolName {
|
||||
continue
|
||||
}
|
||||
return len(tool.Properties) == 0
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func atlascloudEmptyArgumentFallbackTextToolCall(toolName string) string {
|
||||
if toolName == "" {
|
||||
return ""
|
||||
}
|
||||
return `<tool_call name="` + toolName + `">{}</tool_call>`
|
||||
}
|
||||
|
||||
func atlascloudPlanFallbackTextToolCall(prompt string) string {
|
||||
|
||||
@@ -628,76 +628,6 @@ func TestProvider_GenerateFallsBackAfterRepeatedPartialDelegateTextToolCall(t *t
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateFallsBackAfterRepeatedPartialNoArgumentServiceTextToolCall(t *testing.T) {
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"task_TaskService_List\">"}}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
p := NewProvider(
|
||||
ai.WithAPIKey("test-key"),
|
||||
ai.WithBaseURL(ts.URL),
|
||||
ai.WithModel("minimaxai/minimax-m3"),
|
||||
)
|
||||
resp, err := p.Generate(context.Background(), &ai.Request{
|
||||
Prompt: "list the current launch-readiness tasks",
|
||||
Tools: []ai.Tool{{
|
||||
Name: "task_TaskService_List",
|
||||
OriginalName: "task.TaskService.List",
|
||||
Description: "List persisted launch-readiness tasks",
|
||||
Properties: map[string]any{},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned error: %v", err)
|
||||
}
|
||||
want := `<tool_call name="task_TaskService_List">{}</tool_call>`
|
||||
if resp.Reply != want {
|
||||
t.Fatalf("Reply = %q, want %q", resp.Reply, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateFallsBackAfterRepeatedPartialWorkspaceServiceTextToolCall(t *testing.T) {
|
||||
var bodies []map[string]any
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
var body map[string]any
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||
t.Fatalf("decode request: %v", err)
|
||||
}
|
||||
bodies = append(bodies, body)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"workspace_WorkspaceService_Create\">"}}]}`))
|
||||
}))
|
||||
defer ts.Close()
|
||||
|
||||
p := NewProvider(
|
||||
ai.WithAPIKey("test-key"),
|
||||
ai.WithBaseURL(ts.URL),
|
||||
ai.WithModel("minimaxai/minimax-m3"),
|
||||
)
|
||||
resp, err := p.Generate(context.Background(), &ai.Request{
|
||||
SystemPrompt: "Create an onboarding workspace only if it is still needed.",
|
||||
Prompt: "Onboard alice@acme.com. The workspace create side effect may already be complete; avoid failing the flow on a duplicate repaired call.",
|
||||
Tools: []ai.Tool{{
|
||||
Name: "workspace_WorkspaceService_Create",
|
||||
OriginalName: "workspace.WorkspaceService.Create",
|
||||
Description: "Create an onboarding workspace",
|
||||
Properties: map[string]any{"owner": map[string]any{"type": "string"}},
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("Generate returned error: %v", err)
|
||||
}
|
||||
want := `<tool_call name="workspace_WorkspaceService_Create">{"owner":"alice@acme.com"}</tool_call>`
|
||||
if resp.Reply != want {
|
||||
t.Fatalf("Reply = %q, want %q", resp.Reply, want)
|
||||
}
|
||||
if len(bodies) != 2 {
|
||||
t.Fatalf("requests = %d, want initial plus repair", len(bodies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestProvider_GenerateRetriesMinimaxBuiltInsAsTextTools(t *testing.T) {
|
||||
var bodies []map[string]any
|
||||
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Package flow is maintained for backward compatibility.
|
||||
// The canonical import is go-micro.dev/v6/flow.
|
||||
package flow
|
||||
|
||||
import "go-micro.dev/v6/flow"
|
||||
|
||||
// Re-export types for backward compatibility.
|
||||
type Flow = flow.Flow
|
||||
type Options = flow.Options
|
||||
type Option = flow.Option
|
||||
type Result = flow.Result
|
||||
|
||||
var New = flow.New
|
||||
var Trigger = flow.Trigger
|
||||
var Prompt = flow.Prompt
|
||||
var SystemPrompt = flow.SystemPrompt
|
||||
var Provider = flow.Provider
|
||||
var APIKey = flow.APIKey
|
||||
var Model = flow.Model
|
||||
var BaseURL = flow.BaseURL
|
||||
var HistoryLimit = flow.HistoryLimit
|
||||
var OnResult = flow.OnResult
|
||||
@@ -69,50 +69,6 @@ func TestGenerateWithRetryDoesNotRetryCallerCancellation(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithRetryCancellationDuringBackoffStopsRetry(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
attempts := 0
|
||||
firstAttemptDone := make(chan struct{})
|
||||
model := retryModel{generate: func(context.Context, *Request, ...GenerateOption) (*Response, error) {
|
||||
attempts++
|
||||
if attempts == 1 {
|
||||
close(firstAttemptDone)
|
||||
return nil, retryAfterErr{delay: time.Hour}
|
||||
}
|
||||
return &Response{Reply: "unexpected retry"}, nil
|
||||
}}
|
||||
|
||||
errc := make(chan error, 1)
|
||||
go func() {
|
||||
_, err := GenerateWithRetry(ctx, model, &Request{Prompt: "hi"}, GeneratePolicy{
|
||||
MaxAttempts: 3,
|
||||
Backoff: time.Hour,
|
||||
})
|
||||
errc <- err
|
||||
}()
|
||||
|
||||
select {
|
||||
case <-firstAttemptDone:
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("first provider attempt did not run")
|
||||
}
|
||||
cancel()
|
||||
|
||||
select {
|
||||
case err := <-errc:
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("error = %v, want context.Canceled", err)
|
||||
}
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("GenerateWithRetry did not stop after cancellation during backoff")
|
||||
}
|
||||
if attempts != 1 {
|
||||
t.Fatalf("attempts = %d, want cancellation to prevent retry", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGenerateWithRetryHonorsPerAttemptTimeout(t *testing.T) {
|
||||
var attempts atomic.Int32
|
||||
model := retryModel{generate: func(ctx context.Context, _ *Request, _ ...GenerateOption) (*Response, error) {
|
||||
|
||||
+2
-3
@@ -62,8 +62,7 @@ curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
|
||||
## First agent on-ramp
|
||||
|
||||
Once the scaffold → run → call path works, ask the installed CLI for the
|
||||
provider-free agent path. The focused no-secret docs/CLI contract is
|
||||
`make docs-wayfinding`:
|
||||
provider-free agent path:
|
||||
|
||||
```
|
||||
micro agent demo
|
||||
@@ -685,7 +684,7 @@ micro loop init \
|
||||
```
|
||||
|
||||
- `--roles`: which roles to scaffold (`planner,builder,triage`, or `all`)
|
||||
- `--agent`: how the workflows summon the agent — any `@mention`-driven coding agent (e.g. `@codex`, `@claude`)
|
||||
- `--agent`: how the workflows summon the agent (an `@mention`)
|
||||
- `--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,32 +14,6 @@ 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
|
||||
@@ -98,18 +72,6 @@ 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",
|
||||
@@ -262,44 +224,14 @@ 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,46 +59,6 @@ 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,24 +73,3 @@ 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -246,17 +246,17 @@ func Compose(c *cli.Context) error {
|
||||
imageName = registry + "/" + imageName
|
||||
}
|
||||
|
||||
fmt.Fprintf(&sb, " %s:\n", svc.Name)
|
||||
fmt.Fprintf(&sb, " image: %s\n", imageName)
|
||||
sb.WriteString(fmt.Sprintf(" %s:\n", svc.Name))
|
||||
sb.WriteString(fmt.Sprintf(" image: %s\n", imageName))
|
||||
|
||||
if svc.Port > 0 {
|
||||
fmt.Fprintf(&sb, " ports:\n - \"%d:%d\"\n", svc.Port, svc.Port)
|
||||
sb.WriteString(fmt.Sprintf(" ports:\n - \"%d:%d\"\n", svc.Port, svc.Port))
|
||||
}
|
||||
|
||||
if len(svc.Depends) > 0 {
|
||||
sb.WriteString(" depends_on:\n")
|
||||
for _, dep := range svc.Depends {
|
||||
fmt.Fprintf(&sb, " - %s\n", dep)
|
||||
sb.WriteString(fmt.Sprintf(" - %s\n", dep))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -82,9 +82,6 @@ const docsWayfinding = `First-agent and 0→hero docs:
|
||||
prove service tools, mock-model chat, and inspectable run history without
|
||||
configuring a provider key.
|
||||
|
||||
If scaffold → run → chat → inspect stalls, print the short recovery map:
|
||||
micro agent quickcheck
|
||||
|
||||
2. No-secret first-agent transcript
|
||||
https://go-micro.dev/docs/guides/no-secret-first-agent.html
|
||||
Run the maintained support agent without a provider key:
|
||||
|
||||
@@ -93,7 +93,7 @@ func showDeployTargets(cfg *config.Config) error {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("Available deploy targets:\n\n")
|
||||
for name, dt := range cfg.Deploy {
|
||||
fmt.Fprintf(&sb, " %s -> %s\n", name, dt.SSH)
|
||||
sb.WriteString(fmt.Sprintf(" %s -> %s\n", name, dt.SSH))
|
||||
}
|
||||
sb.WriteString("\nDeploy with: micro deploy <target>")
|
||||
return fmt.Errorf("%s", sb.String())
|
||||
|
||||
@@ -502,18 +502,18 @@ func newModel(provider, apiKey, model string) ai.Model {
|
||||
|
||||
func buildProto(dehyphen, titleName string, svc ServiceSpec) string {
|
||||
var b strings.Builder
|
||||
fmt.Fprintf(&b, "syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen)
|
||||
b.WriteString(fmt.Sprintf("syntax = \"proto3\";\n\npackage %s;\n\noption go_package = \"./proto;%s\";\n\n", dehyphen, dehyphen))
|
||||
|
||||
fmt.Fprintf(&b, "service %s {\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("service %s {\n", titleName))
|
||||
for _, ep := range svc.Endpoints {
|
||||
fmt.Fprintf(&b, "\trpc %s(%sRequest) returns (%sResponse) {}\n", ep.Name, ep.Name, ep.Name)
|
||||
b.WriteString(fmt.Sprintf("\trpc %s(%sRequest) returns (%sResponse) {}\n", ep.Name, ep.Name, ep.Name))
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
|
||||
// Record message
|
||||
fmt.Fprintf(&b, "message %sRecord {\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("message %sRecord {\n", titleName))
|
||||
for i, f := range svc.Fields {
|
||||
fmt.Fprintf(&b, "\t%s %s = %d; // %s\n", protoType(f.Type), f.Name, i+1, f.Description)
|
||||
b.WriteString(fmt.Sprintf("\t%s %s = %d; // %s\n", protoType(f.Type), f.Name, i+1, f.Description))
|
||||
}
|
||||
b.WriteString("}\n\n")
|
||||
|
||||
@@ -527,12 +527,12 @@ func buildProto(dehyphen, titleName string, svc ServiceSpec) string {
|
||||
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "\t%s %s = %d;\n", protoType(f.Type), f.Name, n)
|
||||
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
|
||||
n++
|
||||
}
|
||||
fmt.Fprintf(&b, "}\n\nmessage CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("}\n\nmessage CreateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
|
||||
case "Read":
|
||||
fmt.Fprintf(&b, "message ReadRequest {\n\tstring id = 1;\n}\n\nmessage ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("message ReadRequest {\n\tstring id = 1;\n}\n\nmessage ReadResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
|
||||
case "Update":
|
||||
b.WriteString("message UpdateRequest {\n\tstring id = 1;\n")
|
||||
n := 2
|
||||
@@ -540,26 +540,26 @@ func buildProto(dehyphen, titleName string, svc ServiceSpec) string {
|
||||
if f.Name == "id" || f.Name == "created" || f.Name == "updated" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "\t%s %s = %d;\n", protoType(f.Type), f.Name, n)
|
||||
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
|
||||
n++
|
||||
}
|
||||
fmt.Fprintf(&b, "}\n\nmessage UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("}\n\nmessage UpdateResponse {\n\t%sRecord record = 1;\n}\n\n", titleName))
|
||||
case "Delete":
|
||||
b.WriteString("message DeleteRequest {\n\tstring id = 1;\n}\n\nmessage DeleteResponse {\n\tbool deleted = 1;\n}\n\n")
|
||||
case "List":
|
||||
fmt.Fprintf(&b, "message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n\tstring query = 3;\n}\n\nmessage ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName)
|
||||
b.WriteString(fmt.Sprintf("message ListRequest {\n\tint64 limit = 1;\n\tint64 offset = 2;\n\tstring query = 3;\n}\n\nmessage ListResponse {\n\trepeated %sRecord records = 1;\n\tint64 total = 2;\n}\n\n", titleName))
|
||||
default:
|
||||
// Custom endpoint — use all fields as input, record as output
|
||||
fmt.Fprintf(&b, "message %sRequest {\n", ep.Name)
|
||||
b.WriteString(fmt.Sprintf("message %sRequest {\n", ep.Name))
|
||||
n := 1
|
||||
for _, f := range svc.Fields {
|
||||
if f.Name == "created" || f.Name == "updated" {
|
||||
continue
|
||||
}
|
||||
fmt.Fprintf(&b, "\t%s %s = %d;\n", protoType(f.Type), f.Name, n)
|
||||
b.WriteString(fmt.Sprintf("\t%s %s = %d;\n", protoType(f.Type), f.Name, n))
|
||||
n++
|
||||
}
|
||||
fmt.Fprintf(&b, "}\n\nmessage %sResponse {\n\t%sRecord record = 1;\n\tstring message = 2;\n\tbool success = 3;\n}\n\n", ep.Name, titleName)
|
||||
b.WriteString(fmt.Sprintf("}\n\nmessage %sResponse {\n\t%sRecord record = 1;\n\tstring message = 2;\n\tbool success = 3;\n}\n\n", ep.Name, titleName))
|
||||
}
|
||||
}
|
||||
return b.String()
|
||||
|
||||
@@ -38,9 +38,6 @@ 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")
|
||||
}
|
||||
@@ -121,28 +118,6 @@ 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 {
|
||||
@@ -175,7 +150,6 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
|
||||
}
|
||||
agent := commandByName(t, "agent")
|
||||
outputs["micro agent demo"] = commandOutput(t, subcommandByName(t, agent, "demo"))
|
||||
outputs["micro agent quickcheck"] = commandOutput(t, subcommandByName(t, agent, "quickcheck"))
|
||||
|
||||
contracts := []struct {
|
||||
name string
|
||||
@@ -187,13 +161,8 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
|
||||
file: filepath.Join(root, "README.md"),
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"micro agent quickcheck",
|
||||
"micro agent preflight",
|
||||
"micro agent doctor",
|
||||
"micro inspect agent <name>",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"make docs-wayfinding",
|
||||
"examples/first-agent/",
|
||||
"examples/support/",
|
||||
"internal/website/docs/guides/no-secret-first-agent.md",
|
||||
@@ -207,13 +176,8 @@ func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
|
||||
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
|
||||
markers: []string{
|
||||
"micro agent demo",
|
||||
"micro agent quickcheck",
|
||||
"micro agent preflight",
|
||||
"micro agent doctor",
|
||||
"micro inspect agent <name>",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"make docs-wayfinding",
|
||||
"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",
|
||||
|
||||
@@ -98,25 +98,16 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
|
||||
fmt.Fprintf(w, " trace=%s", shortID(run.TraceID))
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
writeAgentRunBreadcrumbs(w, name, run)
|
||||
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)
|
||||
}
|
||||
}
|
||||
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 agent history support run-1`, `micro.AgentResume(ctx, agent, "run-1")`, `micro.ResumeStreamAsk(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.AgentResume(ctx, agent, "run-1")`} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("output missing %q:\n%s", want, got)
|
||||
}
|
||||
@@ -31,14 +31,11 @@ func TestWriteAgentInspectionIncludesInputResumeBreadcrumb(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got := out.String()
|
||||
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro agent history support run-input`, `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
|
||||
for _, want := range []string{"checkpoint=paused", "stage=input-required", `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 — any @mention-driven coding agent (e.g. @codex, @claude)", Value: "@codex"},
|
||||
&cli.StringFlag{Name: "agent", Usage: "How the workflows summon the agent (an @mention)", 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 * * * *"},
|
||||
|
||||
+37
-84
@@ -36,9 +36,6 @@ type subscriber struct {
|
||||
retryLimit int
|
||||
autoAck bool
|
||||
ackWait time.Duration
|
||||
|
||||
pending []Event
|
||||
notify chan struct{}
|
||||
}
|
||||
|
||||
type mem struct {
|
||||
@@ -127,7 +124,6 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
|
||||
retryMap: map[string]int{},
|
||||
autoAck: true,
|
||||
retryLimit: options.GetRetryLimit(),
|
||||
notify: make(chan struct{}, 1),
|
||||
}
|
||||
|
||||
if !options.AutoAck {
|
||||
@@ -136,7 +132,6 @@ func (m *mem) Consume(topic string, opts ...ConsumeOption) (<-chan Event, error)
|
||||
}
|
||||
sub.autoAck = options.AutoAck
|
||||
sub.ackWait = options.AckWait
|
||||
go sub.dispatchManualAck()
|
||||
}
|
||||
|
||||
// register the subscriber
|
||||
@@ -202,98 +197,56 @@ func (m *mem) handleEvent(ev *Event) {
|
||||
}
|
||||
|
||||
func sendEvent(ev *Event, sub *subscriber) {
|
||||
evCopy := *ev
|
||||
if !sub.autoAck {
|
||||
sub.Lock()
|
||||
sub.pending = append(sub.pending, evCopy)
|
||||
sub.Unlock()
|
||||
sub.wake()
|
||||
return
|
||||
}
|
||||
|
||||
go func(s *subscriber) {
|
||||
s.Channel <- evCopy
|
||||
}(sub)
|
||||
}
|
||||
|
||||
func (s *subscriber) wake() {
|
||||
select {
|
||||
case s.notify <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscriber) dispatchManualAck() {
|
||||
for {
|
||||
s.Lock()
|
||||
for len(s.pending) == 0 {
|
||||
s.Unlock()
|
||||
<-s.notify
|
||||
s.Lock()
|
||||
}
|
||||
ev := s.pending[0]
|
||||
s.pending = s.pending[1:]
|
||||
s.retryMap[ev.ID] = 0
|
||||
s.Unlock()
|
||||
|
||||
s.deliverManualAck(ev)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *subscriber) deliverManualAck(ev Event) {
|
||||
retries := 0
|
||||
for {
|
||||
if s.retryLimit > -1 && retries > s.retryLimit {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("Message retry limit reached, discarding: %v %d %d", ev.ID, retries, s.retryLimit)
|
||||
}
|
||||
s.Lock()
|
||||
delete(s.retryMap, ev.ID)
|
||||
s.Unlock()
|
||||
evCopy := *ev
|
||||
if s.autoAck {
|
||||
s.Channel <- evCopy
|
||||
return
|
||||
}
|
||||
|
||||
result := make(chan bool, 1)
|
||||
evCopy := ev
|
||||
evCopy.SetAckFunc(func() error {
|
||||
select {
|
||||
case result <- true:
|
||||
default:
|
||||
evCopy.SetAckFunc(ackFunc(s, evCopy))
|
||||
evCopy.SetNackFunc(nackFunc(s, evCopy))
|
||||
s.Lock()
|
||||
s.retryMap[evCopy.ID] = 0
|
||||
s.Unlock()
|
||||
tick := time.NewTicker(s.ackWait)
|
||||
defer tick.Stop()
|
||||
for range tick.C {
|
||||
s.Lock()
|
||||
count, ok := s.retryMap[evCopy.ID]
|
||||
s.Unlock()
|
||||
if !ok {
|
||||
// success
|
||||
break
|
||||
}
|
||||
return nil
|
||||
})
|
||||
evCopy.SetNackFunc(func() error {
|
||||
select {
|
||||
case result <- false:
|
||||
default:
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
s.Channel <- evCopy
|
||||
|
||||
timer := time.NewTimer(s.ackWait)
|
||||
select {
|
||||
case acked := <-result:
|
||||
if !timer.Stop() {
|
||||
select {
|
||||
case <-timer.C:
|
||||
default:
|
||||
if s.retryLimit > -1 && count > s.retryLimit {
|
||||
if logger.V(logger.ErrorLevel, logger.DefaultLogger) {
|
||||
logger.Errorf("Message retry limit reached, discarding: %v %d %d", evCopy.ID, count, s.retryLimit)
|
||||
}
|
||||
}
|
||||
if acked {
|
||||
s.Lock()
|
||||
delete(s.retryMap, ev.ID)
|
||||
delete(s.retryMap, evCopy.ID)
|
||||
s.Unlock()
|
||||
return
|
||||
}
|
||||
retries++
|
||||
case <-timer.C:
|
||||
retries++
|
||||
s.Channel <- evCopy
|
||||
s.Lock()
|
||||
s.retryMap[evCopy.ID] = count + 1
|
||||
s.Unlock()
|
||||
}
|
||||
}(sub)
|
||||
}
|
||||
|
||||
func ackFunc(s *subscriber, evCopy Event) func() error {
|
||||
return func() error {
|
||||
s.Lock()
|
||||
s.retryMap[ev.ID] = retries
|
||||
delete(s.retryMap, evCopy.ID)
|
||||
s.Unlock()
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func nackFunc(_ *subscriber, _ Event) func() error {
|
||||
return func() error {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,7 +157,6 @@ func runTestStream(t *testing.T, stream Stream) {
|
||||
assert.NoError(t, err, "Unexpected error subscribing")
|
||||
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 1"}))
|
||||
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 2"}))
|
||||
assert.NoError(t, stream.Publish("foobarAck", map[string]string{"foo": "message 3"}))
|
||||
|
||||
ev := <-ch
|
||||
ev.Ack()
|
||||
@@ -171,13 +170,6 @@ func runTestStream(t *testing.T, stream Stream) {
|
||||
case <-time.After(7 * time.Second):
|
||||
t.Fatalf("Timed out waiting for message to be put back on queue")
|
||||
}
|
||||
select {
|
||||
case ev = <-ch:
|
||||
assert.NotEqual(t, ev.ID, nacked, "Queued message should only be received after the nacked message is redelivered")
|
||||
assert.NoError(t, ev.Ack())
|
||||
case <-time.After(7 * time.Second):
|
||||
t.Fatalf("Timed out waiting for queued message")
|
||||
}
|
||||
|
||||
})
|
||||
|
||||
|
||||
@@ -34,22 +34,5 @@ CI keeps this path runnable with:
|
||||
go test ./examples/first-agent
|
||||
```
|
||||
|
||||
## Next chat, inspect, and debug breadcrumbs
|
||||
|
||||
This example exits after one in-process `assistant.Ask` call so it stays tiny and
|
||||
provider-free. When you move from this transcript to a long-running agent, keep
|
||||
these commands nearby:
|
||||
|
||||
```bash
|
||||
micro run
|
||||
micro chat assistant --prompt "Summarize my next steps"
|
||||
micro inspect agent assistant
|
||||
micro agent doctor assistant
|
||||
```
|
||||
|
||||
Use the [no-secret first-agent guide](../../internal/website/docs/guides/no-secret-first-agent.md)
|
||||
to compare this transcript with the CLI demo, then keep the
|
||||
[debugging guide](../../internal/website/docs/guides/debugging-agents.md) open for
|
||||
preflight, doctor, inspect, and history checks. After that, continue to
|
||||
[`examples/support`](../support/) for the full services → agents → workflows
|
||||
lifecycle with a flow trigger and an approval gate.
|
||||
After this, continue to [`examples/support`](../support/) for the full services →
|
||||
agents → workflows lifecycle with a flow trigger and an approval gate.
|
||||
|
||||
@@ -13,7 +13,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -34,13 +33,13 @@ type ListNotesResponse struct {
|
||||
Notes []string `json:"notes" description:"Notes the assistant can summarize"`
|
||||
}
|
||||
|
||||
type NotesService struct{ w io.Writer }
|
||||
type NotesService struct{}
|
||||
|
||||
// List returns the starter notes the first agent can read.
|
||||
// @example {}
|
||||
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
|
||||
rsp.Notes = []string{"Install the micro CLI", "Run a service", "Chat with an agent"}
|
||||
fmt.Fprintln(s.w, " [notes] listed starter notes")
|
||||
fmt.Println(" [notes] listed starter notes")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -91,10 +90,6 @@ func waitFor(reg registry.Registry, names ...string) error {
|
||||
}
|
||||
|
||||
func runFirstAgent() error {
|
||||
return runFirstAgentWithWriter(os.Stdout)
|
||||
}
|
||||
|
||||
func runFirstAgentWithWriter(w io.Writer) error {
|
||||
ai.Register("first-agent-mock", newMock)
|
||||
|
||||
reg := registry.NewMemoryRegistry()
|
||||
@@ -109,7 +104,7 @@ func runFirstAgentWithWriter(w io.Writer) error {
|
||||
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))), client.Broker(br))
|
||||
|
||||
notes := service.New(service.Name("notes"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.Broker(br), service.HandleSignal(false))
|
||||
if err := notes.Handle(&NotesService{w: w}); err != nil {
|
||||
if err := notes.Handle(new(NotesService)); err != nil {
|
||||
return fmt.Errorf("handle notes: %w", err)
|
||||
}
|
||||
svcErr := make(chan error, 1)
|
||||
@@ -142,14 +137,14 @@ func runFirstAgentWithWriter(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
fmt.Fprintln(w, "First agent (provider: mock, no API key)")
|
||||
fmt.Fprintln(w, "> Summarize my next steps")
|
||||
fmt.Println("First agent (provider: mock, no API key)")
|
||||
fmt.Println("> Summarize my next steps")
|
||||
resp, err := assistant.Ask(context.Background(), "Summarize my next steps")
|
||||
if err != nil {
|
||||
return fmt.Errorf("ask assistant: %w", err)
|
||||
}
|
||||
fmt.Fprintln(w, "assistant:", resp.Reply)
|
||||
fmt.Fprintln(w, "✓ service-backed agent completed without provider secrets")
|
||||
fmt.Println("assistant:", resp.Reply)
|
||||
fmt.Println("✓ service-backed agent completed without provider secrets")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -1,71 +1,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
import "testing"
|
||||
|
||||
func TestRunFirstAgent(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
if err := runFirstAgentWithWriter(&out); err != nil {
|
||||
if err := runFirstAgent(); err != nil {
|
||||
t.Fatalf("first-agent example failed: %v", err)
|
||||
}
|
||||
|
||||
want := strings.TrimSpace(readExpectedTranscript(t))
|
||||
got := strings.TrimSpace(out.String())
|
||||
if got != want {
|
||||
t.Fatalf("first-agent transcript drifted from README.md\n--- got ---\n%s\n--- want ---\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadmeDocumentsNextBreadcrumbs(t *testing.T) {
|
||||
b, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
readme := string(b)
|
||||
start := strings.Index(readme, "## Next chat, inspect, and debug breadcrumbs")
|
||||
if start < 0 {
|
||||
t.Fatal("README.md missing next chat, inspect, and debug breadcrumbs section")
|
||||
}
|
||||
section := readme[start:]
|
||||
for _, want := range []string{
|
||||
"micro run",
|
||||
"micro chat assistant --prompt \"Summarize my next steps\"",
|
||||
"micro inspect agent assistant",
|
||||
"micro agent doctor assistant",
|
||||
"no-secret-first-agent.md",
|
||||
"debugging-agents.md",
|
||||
"examples/support",
|
||||
} {
|
||||
if !strings.Contains(section, want) {
|
||||
t.Fatalf("README.md next breadcrumbs missing %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func readExpectedTranscript(t *testing.T) string {
|
||||
t.Helper()
|
||||
b, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
readme := string(b)
|
||||
const fence = "```text"
|
||||
start := strings.Index(readme, "Expected transcript:")
|
||||
if start < 0 {
|
||||
t.Fatal("README.md missing Expected transcript section")
|
||||
}
|
||||
fenceStart := strings.Index(readme[start:], fence)
|
||||
if fenceStart < 0 {
|
||||
t.Fatal("README.md missing transcript text fence")
|
||||
}
|
||||
start += fenceStart + len(fence)
|
||||
end := strings.Index(readme[start:], "```")
|
||||
if end < 0 {
|
||||
t.Fatal("README.md missing closing transcript fence")
|
||||
}
|
||||
return readme[start : start+end]
|
||||
}
|
||||
|
||||
@@ -54,32 +54,6 @@ agent, which:
|
||||
emailing a customer (`notify.Send`) passes through the gate first. Return
|
||||
`false` to hold it for a person or a policy; the example approves and logs.
|
||||
|
||||
## Expected inspect transcript
|
||||
|
||||
The provider-free run prints the same visible checkpoints a new developer should
|
||||
compare against after chat and flow execution. The transcript includes service
|
||||
tool calls, the approval gate, and the inspect/run-history commands that prove
|
||||
the workflow run was recorded.
|
||||
|
||||
```text
|
||||
> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}
|
||||
|
||||
[customers] looked up Alice (pro plan)
|
||||
[tickets] ticket-1 → priority=high status=in_progress
|
||||
▣ approval gate notify_NotifyService_Send(alice@acme.com) — approved
|
||||
[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."
|
||||
|
||||
support agent: Triaged ticket-1 for Alice and sent a reply.
|
||||
|
||||
inspect transcript:
|
||||
micro inspect flow intake
|
||||
flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."
|
||||
micro agent history support
|
||||
agent: support runs=1 latest.status=completed
|
||||
|
||||
✓ ticket triaged and the customer was replied to — triggered by an event
|
||||
```
|
||||
|
||||
## Run
|
||||
|
||||
```bash
|
||||
|
||||
@@ -302,13 +302,7 @@ func runSupport(provider string) error {
|
||||
}
|
||||
|
||||
if rs := intake.Results(); len(rs) > 0 {
|
||||
latest := rs[len(rs)-1]
|
||||
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", latest.Reply)
|
||||
fmt.Println("\n\033[1minspect transcript:\033[0m")
|
||||
fmt.Println(" micro inspect flow intake")
|
||||
fmt.Printf(" flow: intake runs=%d latest.reply=%q\n", len(rs), latest.Reply)
|
||||
fmt.Println(" micro agent history support")
|
||||
fmt.Printf(" agent: support runs=%d latest.status=completed\n", len(rs))
|
||||
fmt.Printf("\n\033[1msupport agent:\033[0m %s\n", rs[len(rs)-1].Reply)
|
||||
}
|
||||
if notify.sent >= 1 {
|
||||
fmt.Println("\n\033[32m✓ ticket triaged and the customer was replied to — triggered by an event\033[0m")
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
@@ -33,78 +30,3 @@ func TestZeroToHeroReadmeDocumentsLifecycle(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestZeroToHeroInspectTranscript(t *testing.T) {
|
||||
out := captureStdout(t, func() {
|
||||
if err := runSupport("mock"); err != nil {
|
||||
t.Fatalf("support example failed: %v", err)
|
||||
}
|
||||
})
|
||||
got := stripANSI(out)
|
||||
|
||||
for _, want := range []string{
|
||||
`> event: events.ticket.created {"customer":"alice@acme.com","id":"ticket-1","subject":"Can't log in"}`,
|
||||
`[customers] looked up Alice (pro plan)`,
|
||||
`[tickets] ticket-1 → priority=high status=in_progress`,
|
||||
`approval gate notify_NotifyService_Send(alice@acme.com) — approved`,
|
||||
`[notify] 📨 to=alice@acme.com: "Hi Alice — thanks for reaching out. We've bumped this to high priority and are on it."`,
|
||||
`support agent: Triaged ticket-1 for Alice and sent a reply.`,
|
||||
`inspect transcript:`,
|
||||
`micro inspect flow intake`,
|
||||
`flow: intake runs=1 latest.reply="Triaged ticket-1 for Alice and sent a reply."`,
|
||||
`micro agent history support`,
|
||||
`agent: support runs=1 latest.status=completed`,
|
||||
`✓ ticket triaged and the customer was replied to — triggered by an event`,
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("support transcript missing %q\n--- got ---\n%s", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
readme, err := os.ReadFile("README.md")
|
||||
if err != nil {
|
||||
t.Fatalf("read README.md: %v", err)
|
||||
}
|
||||
for _, want := range []string{
|
||||
"Expected inspect transcript",
|
||||
"micro inspect flow intake",
|
||||
"micro agent history support",
|
||||
"agent: support runs=1 latest.status=completed",
|
||||
} {
|
||||
if !strings.Contains(string(readme), want) {
|
||||
t.Fatalf("README.md missing transcript contract %q", want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func captureStdout(t *testing.T, fn func()) (out string) {
|
||||
t.Helper()
|
||||
old := os.Stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatalf("capture stdout: %v", err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
var buf bytes.Buffer
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
_, _ = io.Copy(&buf, r)
|
||||
close(done)
|
||||
}()
|
||||
defer func() {
|
||||
_ = w.Close()
|
||||
os.Stdout = old
|
||||
<-done
|
||||
out = buf.String()
|
||||
}()
|
||||
|
||||
fn()
|
||||
return out
|
||||
}
|
||||
|
||||
var ansiRE = regexp.MustCompile(`\x1b\[[0-9;]*m`)
|
||||
|
||||
func stripANSI(s string) string {
|
||||
return ansiRE.ReplaceAllString(s, "")
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
module go-micro.dev/v6
|
||||
|
||||
go 1.25.0
|
||||
go 1.24
|
||||
|
||||
toolchain go1.25.12
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
dario.cat/mergo v1.0.2
|
||||
@@ -32,23 +32,24 @@ 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.11.1
|
||||
github.com/stretchr/testify v1.10.0
|
||||
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.39.0
|
||||
go.opentelemetry.io/otel/sdk v1.39.0
|
||||
go.opentelemetry.io/otel/trace v1.39.0
|
||||
go.opentelemetry.io/otel v1.35.0
|
||||
go.opentelemetry.io/otel/sdk v1.35.0
|
||||
go.opentelemetry.io/otel/trace v1.35.0
|
||||
go.uber.org/zap v1.27.0
|
||||
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
|
||||
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
|
||||
google.golang.org/grpc/examples v0.0.0-20250515150734-f2d3e11f3057
|
||||
google.golang.org/protobuf v1.36.10
|
||||
google.golang.org/protobuf v1.36.6
|
||||
)
|
||||
|
||||
require (
|
||||
@@ -62,7 +63,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.3 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // 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
|
||||
@@ -96,20 +97,19 @@ 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.14.1 // indirect
|
||||
github.com/rogpeppe/go-internal v1.13.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.2.1 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.39.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 // indirect
|
||||
go.opentelemetry.io/otel/metric v1.35.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.35.0 // indirect
|
||||
golang.org/x/sys v0.45.0 // indirect
|
||||
golang.org/x/text v0.37.0 // 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/time v0.11.0 // 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
|
||||
golang.org/x/tools v0.31.0 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250324211829-b45e905df463 // 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.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
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/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.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
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/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.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
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/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.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.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.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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
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/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.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
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/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.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
|
||||
golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
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.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
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.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
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.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
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,26 +499,24 @@ 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.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
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/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=
|
||||
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/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=
|
||||
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.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
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,11 +3,8 @@
|
||||
`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. 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.
|
||||
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.
|
||||
|
||||
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
|
||||
@@ -43,20 +40,7 @@ live model credits.
|
||||
|
||||
Use `make provider-conformance` when you want the live-provider sweep: providers
|
||||
without keys are skipped, and configured providers must satisfy the same harness
|
||||
contract. For the exact scheduled command, run:
|
||||
|
||||
```sh
|
||||
go run ./internal/harness/provider-conformance \
|
||||
-providers anthropic,openai,gemini,groq,minimax,mistral,together,atlascloud \
|
||||
-harnesses agent,universe,agent-flow,plan-delegate,a2a-stream-fallback \
|
||||
-summary-json provider-conformance-summary.json \
|
||||
-summary-markdown provider-conformance-summary.md \
|
||||
-capabilities-markdown provider-capabilities.md
|
||||
```
|
||||
|
||||
The generated summary records one row for every selected provider/harness pair;
|
||||
missing live-provider keys become skipped rows for each harness, while configured
|
||||
providers produce pass/fail rows per harness.
|
||||
contract.
|
||||
|
||||
## Scheduled CI
|
||||
|
||||
|
||||
@@ -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`, `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. |
|
||||
| **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. |
|
||||
| **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. |
|
||||
|
||||
@@ -99,7 +99,7 @@ type NotifyService struct {
|
||||
// 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))
|
||||
key := req.To + "\x00" + req.Message
|
||||
s.mu.Lock()
|
||||
if s.sent == nil {
|
||||
s.sent = make(map[string]bool)
|
||||
@@ -202,22 +202,15 @@ func waitFor(reg registry.Registry, name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func waitForOnboardingSideEffects(ctx context.Context, wsSvc *WorkspaceService, ntSvc *NotifyService, recoverMissingNotify func(context.Context) error) error {
|
||||
func waitForOnboardingSideEffects(ctx context.Context, wsSvc *WorkspaceService, ntSvc *NotifyService) 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():
|
||||
@@ -311,11 +304,7 @@ 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, 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
|
||||
})
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc)
|
||||
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, nil)
|
||||
err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc)
|
||||
if err == nil {
|
||||
t.Fatal("waitForOnboardingSideEffects returned nil, want missing side effects error")
|
||||
}
|
||||
@@ -131,37 +131,11 @@ func TestWaitForOnboardingSideEffectsPassesWhenComplete(t *testing.T) {
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||
defer cancel()
|
||||
if err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc, nil); err != nil {
|
||||
if err := waitForOnboardingSideEffects(ctx, wsSvc, ntSvc); 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)
|
||||
@@ -195,17 +169,3 @@ func TestNotifySendSuppressesDuplicateMessage(t *testing.T) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -474,11 +474,10 @@ func runPlanDelegate(provider string) error {
|
||||
agent.Name("conductor"),
|
||||
agent.Address("127.0.0.1:0"),
|
||||
agent.Services("task"),
|
||||
agent.Prompt("You coordinate launch work. Before any task or delegate tool call, you must persist the launch-readiness plan with the built-in plan tool. Then create exactly one Design task, one Build task, and one Ship task, then delegate exactly one readiness notification to the \"comms\" agent. Do not create duplicate tasks and do not send notifications yourself."),
|
||||
agent.Prompt("You coordinate launch work. Plan first, create exactly one Design task, one Build task, and one Ship task, then delegate exactly one readiness notification to the \"comms\" agent. Do not create duplicate tasks and do not send notifications yourself."),
|
||||
agent.Provider(provider), agent.APIKey(apiKey),
|
||||
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
|
||||
agent.WithCheckpoint(conductorCheckpoint),
|
||||
agent.WrapTool(requirePersistedPlanBeforeConductorActions(mem)),
|
||||
}
|
||||
conductorOpts = append(conductorOpts, liveAgentOpts...)
|
||||
conductor := agent.New(conductorOpts...)
|
||||
@@ -532,8 +531,11 @@ func runPlanDelegate(provider string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := requireConductorPlan(context.Background(), mem, conductor); err != nil {
|
||||
return err
|
||||
// Prove plan was persisted to the real store.
|
||||
if recs, _ := store.Scope(mem, "agent", "conductor").Read("plan"); len(recs) > 0 {
|
||||
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
|
||||
} else {
|
||||
return fmt.Errorf("plan was not persisted")
|
||||
}
|
||||
if taskSvc.count() == 0 || notifySvc.count() != 1 {
|
||||
return fmt.Errorf("unexpected side effects: tasks=%d notify=%d", taskSvc.count(), notifySvc.count())
|
||||
@@ -543,46 +545,6 @@ func runPlanDelegate(provider string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func requirePersistedPlanBeforeConductorActions(mem store.Store) ai.ToolWrapper {
|
||||
return func(next ai.ToolHandler) ai.ToolHandler {
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
if call.Name == "plan" {
|
||||
return next(ctx, call)
|
||||
}
|
||||
if recs, err := store.Scope(mem, "agent", "conductor").Read("plan"); err == nil && len(recs) > 0 {
|
||||
return next(ctx, call)
|
||||
}
|
||||
msg := "persist the launch-readiness plan first by calling the built-in plan tool before task or delegate side effects"
|
||||
return ai.ToolResult{
|
||||
ID: call.ID,
|
||||
Value: map[string]string{"error": msg},
|
||||
Content: `{"error":"` + msg + `"}`,
|
||||
Refused: ai.RefusedApproval,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func requireConductorPlan(ctx context.Context, mem store.Store, conductor agent.Agent) error {
|
||||
recs, _ := store.Scope(mem, "agent", "conductor").Read("plan")
|
||||
if len(recs) == 0 && conductor != nil {
|
||||
fmt.Print("\n\033[33mwarning:\033[0m conductor completed side effects without a persisted plan; retrying plan persistence once before final assertions.\n")
|
||||
_, err := conductor.Ask(ctx, "Persist the launch-readiness plan now using the built-in plan tool before answering. Record exactly these completed steps: Design launch task, Build launch task, Ship launch task, and delegate owner readiness notification to comms. Do not call task or notify tools.")
|
||||
if err != nil {
|
||||
return fmt.Errorf("plan was not persisted at agent/conductor/plan and recovery prompt failed after completed side effects: %w", err)
|
||||
}
|
||||
recs, _ = store.Scope(mem, "agent", "conductor").Read("plan")
|
||||
}
|
||||
if len(recs) == 0 {
|
||||
return fmt.Errorf("plan was not persisted at agent/conductor/plan; conductor completed task/notify side effects without calling the built-in plan tool")
|
||||
}
|
||||
if len(recs) != 1 {
|
||||
return fmt.Errorf("unexpected persisted conductor plans at agent/conductor/plan: got %d records, want 1", len(recs))
|
||||
}
|
||||
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
|
||||
return nil
|
||||
}
|
||||
|
||||
func planDelegateConductorStep(conductor agent.Agent, taskSvc *TaskService, notifySvc *NotifyService) flow.StepFunc {
|
||||
return func(ctx context.Context, in flow.State) (flow.State, error) {
|
||||
prompt := "Create three launch tasks (Design, Build, Ship), then make sure owner@acme.com is notified: " + in.String()
|
||||
@@ -597,23 +559,6 @@ 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
|
||||
}
|
||||
}
|
||||
@@ -668,11 +613,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 {
|
||||
@@ -716,10 +661,6 @@ 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)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -368,27 +367,6 @@ 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
|
||||
}
|
||||
@@ -402,142 +380,6 @@ func (a failingAgent) Run() error { re
|
||||
func (a failingAgent) Stop() error { return nil }
|
||||
func (a failingAgent) String() string { return "failing" }
|
||||
|
||||
func TestRequireConductorPlanRecoversAfterCompletedSideEffects(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
ag := &scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
|
||||
func(ctx context.Context, prompt string) (*agent.Response, error) {
|
||||
if !strings.Contains(prompt, "built-in plan tool") || !strings.Contains(prompt, "Do not call task or notify tools") {
|
||||
return nil, errors.New("missing scoped plan recovery prompt")
|
||||
}
|
||||
if err := store.Scope(mem, "agent", "conductor").Write(&store.Record{Key: "plan", Value: []byte(`{"steps":[{"task":"Design launch task","status":"done"}]}`)}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &agent.Response{Reply: "Plan persisted."}, nil
|
||||
},
|
||||
}}
|
||||
|
||||
if err := requireConductorPlan(context.Background(), mem, ag); err != nil {
|
||||
t.Fatalf("requireConductorPlan returned %v, want recovered plan", err)
|
||||
}
|
||||
if ag.calls != 1 {
|
||||
t.Fatalf("conductor calls = %d, want one plan recovery prompt", ag.calls)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequireConductorPlanFailureNamesScopedRecord(t *testing.T) {
|
||||
err := requireConductorPlan(context.Background(), store.NewMemoryStore(), &scriptedAgent{replies: []func(context.Context, string) (*agent.Response, error){
|
||||
func(context.Context, string) (*agent.Response, error) {
|
||||
return &agent.Response{Reply: "Done without plan."}, nil
|
||||
},
|
||||
}})
|
||||
if err == nil {
|
||||
t.Fatal("requireConductorPlan returned nil, want missing plan diagnostic")
|
||||
}
|
||||
for _, want := range []string{"agent/conductor/plan", "without calling the built-in plan tool"} {
|
||||
if got := err.Error(); !strings.Contains(got, want) {
|
||||
t.Fatalf("error = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequirePersistedPlanBeforeConductorActionsBlocksSideEffects(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
called := false
|
||||
wrapped := requirePersistedPlanBeforeConductorActions(mem)(func(context.Context, ai.ToolCall) ai.ToolResult {
|
||||
called = true
|
||||
return ai.ToolResult{ID: "call-1", Content: `{"ok":true}`}
|
||||
})
|
||||
|
||||
res := wrapped(context.Background(), ai.ToolCall{ID: "call-1", Name: "task.Add"})
|
||||
if called {
|
||||
t.Fatal("side-effecting tool ran before persisted plan")
|
||||
}
|
||||
if res.Refused != ai.RefusedApproval {
|
||||
t.Fatalf("Refused = %q, want %q", res.Refused, ai.RefusedApproval)
|
||||
}
|
||||
if !strings.Contains(res.Content, "built-in plan tool") {
|
||||
t.Fatalf("Content = %q, want plan-first steering message", res.Content)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequirePersistedPlanBeforeConductorActionsAllowsPlanAndPlannedActions(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
var calls []string
|
||||
wrapped := requirePersistedPlanBeforeConductorActions(mem)(func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
calls = append(calls, call.Name)
|
||||
if call.Name == "plan" {
|
||||
if err := store.Scope(mem, "agent", "conductor").Write(&store.Record{Key: "plan", Value: []byte(`{"steps":[{"task":"Design","status":"pending"}]}`)}); err != nil {
|
||||
t.Fatalf("write plan: %v", err)
|
||||
}
|
||||
}
|
||||
return ai.ToolResult{ID: call.ID, Content: `{"ok":true}`}
|
||||
})
|
||||
|
||||
if res := wrapped(context.Background(), ai.ToolCall{ID: "call-1", Name: "plan"}); res.Refused != "" {
|
||||
t.Fatalf("plan Refused = %q, want allowed", res.Refused)
|
||||
}
|
||||
if res := wrapped(context.Background(), ai.ToolCall{ID: "call-2", Name: "task.Add"}); res.Refused != "" {
|
||||
t.Fatalf("planned action Refused = %q, want allowed", res.Refused)
|
||||
}
|
||||
if want := []string{"plan", "task.Add"}; !reflect.DeepEqual(calls, want) {
|
||||
t.Fatalf("calls = %v, want %v", calls, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlanDelegateConductorRetriesAfterPlanOnlySuccess(t *testing.T) {
|
||||
taskSvc := new(TaskService)
|
||||
notifySvc := new(NotifyService)
|
||||
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"} {
|
||||
@@ -683,28 +525,6 @@ 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"}`)
|
||||
|
||||
@@ -92,11 +92,11 @@ func main() {
|
||||
if *requireConfiguredFlag {
|
||||
fmt.Printf("FAIL %s: missing API key (%s)\n", provider, msg)
|
||||
failed++
|
||||
results = append(results, missingKeyResults(provider, harnesses, statusFailed, "missing API key: "+msg)...)
|
||||
results = append(results, conformanceResult{Provider: provider, Status: statusFailed, Error: "missing API key: " + msg})
|
||||
} else {
|
||||
fmt.Printf("- %s: skipped (%s)\n", provider, msg)
|
||||
skipped++
|
||||
results = append(results, missingKeyResults(provider, harnesses, statusSkipped, msg)...)
|
||||
results = append(results, conformanceResult{Provider: provider, Status: statusSkipped, Error: msg})
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -166,20 +166,6 @@ type conformanceSummary struct {
|
||||
Failed int `json:"failed"`
|
||||
}
|
||||
|
||||
func missingKeyResults(provider string, harnesses []string, status, detail string) []conformanceResult {
|
||||
results := make([]conformanceResult, 0, len(harnesses))
|
||||
for _, harness := range harnesses {
|
||||
results = append(results, conformanceResult{
|
||||
Provider: provider,
|
||||
Harness: harness,
|
||||
Phase: harnessPhase(harness),
|
||||
Status: status,
|
||||
Error: detail,
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
func writeSummaryJSON(path string, summary conformanceSummary) error {
|
||||
b, err := json.MarshalIndent(summary, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -77,23 +77,6 @@ func TestCapabilityMatrixHasRegisteredProviders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMissingKeyResultsReportsEachHarness(t *testing.T) {
|
||||
results := missingKeyResults("openai", []string{"agent", "plan-delegate"}, statusSkipped, "set OPENAI_API_KEY")
|
||||
if len(results) != 2 {
|
||||
t.Fatalf("missingKeyResults returned %d results, want 2", len(results))
|
||||
}
|
||||
|
||||
wants := []conformanceResult{
|
||||
{Provider: "openai", Harness: "agent", Phase: harnessPhase("agent"), Status: statusSkipped, Error: "set OPENAI_API_KEY"},
|
||||
{Provider: "openai", Harness: "plan-delegate", Phase: harnessPhase("plan-delegate"), Status: statusSkipped, Error: "set OPENAI_API_KEY"},
|
||||
}
|
||||
for i, want := range wants {
|
||||
if results[i] != want {
|
||||
t.Fatalf("result[%d] = %#v, want %#v", i, results[i], want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteCapabilityMarkdown(t *testing.T) {
|
||||
path := filepath.Join(t.TempDir(), "capabilities.md")
|
||||
rows := []ai.CapabilityRow{
|
||||
|
||||
@@ -269,21 +269,13 @@ 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 !wait.Stop() {
|
||||
<-wait.C
|
||||
if dispatchErr != nil {
|
||||
return in, dispatchErr
|
||||
}
|
||||
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:
|
||||
return in, ctx.Err()
|
||||
case <-time.After(25 * time.Millisecond):
|
||||
}
|
||||
}
|
||||
if dispatchErr != nil {
|
||||
@@ -357,55 +349,14 @@ func check(cond bool, format string, args ...any) {
|
||||
// should not depend on a live model deciding to send another notification.
|
||||
func a2aReachable(ctx context.Context, base, agent string) error {
|
||||
probe := "A2A reachability probe only. Reply with the words concierge reachable. Do not call tools or send notifications."
|
||||
deadline, ok := ctx.Deadline()
|
||||
if !ok {
|
||||
deadline = time.Now().Add(10 * time.Second)
|
||||
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(ctx, probe)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var lastErr error
|
||||
for attempt := 1; ; attempt++ {
|
||||
if err := ctx.Err(); err != nil {
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
remaining := time.Until(deadline)
|
||||
if remaining <= 0 {
|
||||
if lastErr != nil {
|
||||
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt-1, lastErr)
|
||||
}
|
||||
return context.DeadlineExceeded
|
||||
}
|
||||
|
||||
attemptTimeout := 4 * time.Second
|
||||
if remaining < attemptTimeout {
|
||||
attemptTimeout = remaining
|
||||
}
|
||||
attemptCtx, cancel := context.WithTimeout(ctx, attemptTimeout)
|
||||
reply, err := a2a.NewClient(base+"/agents/"+agent).Send(attemptCtx, probe)
|
||||
cancel()
|
||||
if err == nil && strings.TrimSpace(reply) != "" {
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
err = fmt.Errorf("empty A2A reply")
|
||||
}
|
||||
lastErr = err
|
||||
|
||||
if time.Until(deadline) <= 0 {
|
||||
return fmt.Errorf("A2A reachability probe failed after %d attempt(s): %w", attempt, lastErr)
|
||||
}
|
||||
time.Sleep(minDuration(200*time.Millisecond*time.Duration(attempt), time.Until(deadline)))
|
||||
if strings.TrimSpace(reply) == "" {
|
||||
return fmt.Errorf("empty A2A reply")
|
||||
}
|
||||
}
|
||||
|
||||
func minDuration(a, b time.Duration) time.Duration {
|
||||
if a < b {
|
||||
return a
|
||||
}
|
||||
return b
|
||||
return nil
|
||||
}
|
||||
|
||||
func providerKey(provider string) string {
|
||||
|
||||
@@ -3,9 +3,6 @@ package main
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -28,31 +25,6 @@ func TestUniverseHarnessContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestA2AReachableRetriesTransientTimeout(t *testing.T) {
|
||||
var calls int64
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got, want := r.URL.Path, "/agents/concierge"; got != want {
|
||||
t.Fatalf("path = %q, want %q", got, want)
|
||||
}
|
||||
if atomic.AddInt64(&calls, 1) == 1 {
|
||||
time.Sleep(5 * time.Second)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
fmt.Fprint(w, `{"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"task-1","contextId":"ctx-1","status":{"state":"completed"},"artifacts":[{"artifactId":"artifact-1","parts":[{"kind":"text","text":"concierge reachable"}]}]}}`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
|
||||
defer cancel()
|
||||
if err := a2aReachable(ctx, srv.URL, "concierge"); err != nil {
|
||||
t.Fatalf("a2aReachable returned error: %v", err)
|
||||
}
|
||||
if got := atomic.LoadInt64(&calls); got < 2 {
|
||||
t.Fatalf("A2A calls = %d, want retry after transient timeout", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNotifyStepCompletesAfterObservedSideEffectTimeout(t *testing.T) {
|
||||
ntf := new(Notify)
|
||||
before := atomic.LoadInt64(&ntf.sent)
|
||||
@@ -97,42 +69,6 @@ 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)
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -27,7 +26,7 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
|
||||
"go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1",
|
||||
"go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1",
|
||||
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
|
||||
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1",
|
||||
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
|
||||
"./internal/harness/zero-to-hero-ci/run.sh",
|
||||
"micro zero-to-hero",
|
||||
"go run ./internal/harness/agent-flow",
|
||||
@@ -46,7 +45,7 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
|
||||
"go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestExamplesWayfindingIndexStaysLinked|TestExamplesCommandPointsAtWayfindingIndex|TestZeroToHeroCLIBoundaries|TestZeroToHeroCommandPrintsMaintainedNoSecretPath' -count=1",
|
||||
"go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1",
|
||||
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
|
||||
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1",
|
||||
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
|
||||
} {
|
||||
if !strings.Contains(runScript, want) {
|
||||
t.Fatalf("0→hero CI run script missing lifecycle command %q", want)
|
||||
@@ -289,13 +288,11 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
heading: "### First agent on-ramp",
|
||||
links: []string{
|
||||
"internal/website/docs/guides/install-troubleshooting.md",
|
||||
"make docs-wayfinding",
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
"internal/website/docs/guides/no-secret-first-agent.md",
|
||||
"internal/website/docs/guides/your-first-agent.md",
|
||||
"micro chat",
|
||||
"internal/website/docs/guides/debugging-agents.md",
|
||||
"micro inspect agent <name>",
|
||||
"internal/website/docs/guides/zero-to-hero.md",
|
||||
@@ -334,7 +331,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
file: filepath.Join(root, "cmd", "micro", "README.md"),
|
||||
heading: "## First agent on-ramp",
|
||||
links: []string{
|
||||
"make docs-wayfinding",
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
@@ -358,7 +354,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
heading: "### First-agent on-ramp",
|
||||
links: []string{
|
||||
"guides/install-troubleshooting.html",
|
||||
"make docs-wayfinding",
|
||||
"micro agent demo",
|
||||
"micro examples",
|
||||
"micro zero-to-hero",
|
||||
@@ -367,7 +362,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
"https://github.com/micro/go-micro/tree/master/examples/first-agent",
|
||||
"guides/no-secret-first-agent.html",
|
||||
"guides/your-first-agent.html",
|
||||
"micro chat",
|
||||
"guides/debugging-agents.html",
|
||||
"micro inspect agent <name>",
|
||||
"guides/zero-to-hero.html",
|
||||
@@ -387,7 +381,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
"https://github.com/micro/go-micro/tree/master/examples/first-agent",
|
||||
"guides/no-secret-first-agent.html",
|
||||
"guides/your-first-agent.html",
|
||||
"micro chat",
|
||||
"guides/debugging-agents.html",
|
||||
"micro inspect agent <name>",
|
||||
"guides/zero-to-hero.html",
|
||||
@@ -403,7 +396,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
"https://github.com/micro/go-micro/tree/master/examples/support",
|
||||
"guides/no-secret-first-agent.html",
|
||||
"guides/your-first-agent.html",
|
||||
"micro chat",
|
||||
"guides/debugging-agents.html",
|
||||
"micro inspect agent <name>",
|
||||
"guides/zero-to-hero.html",
|
||||
@@ -430,108 +422,6 @@ 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",
|
||||
"micro chat",
|
||||
"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",
|
||||
"micro chat",
|
||||
"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 {
|
||||
@@ -653,7 +543,7 @@ func TestFirstAgentGuideChainDocumentsRequiredNextSteps(t *testing.T) {
|
||||
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",
|
||||
"go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfindingDocs -count=1",
|
||||
"micro zero-to-hero",
|
||||
"go run ./examples/first-agent",
|
||||
"go run ./examples/support",
|
||||
@@ -1008,190 +898,6 @@ func TestNoSecretFirstAgentDebuggingSmoke(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentCLIChatInspectFixture(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
absRoot, err := filepath.Abs(root)
|
||||
if err != nil {
|
||||
t.Fatalf("resolve repository root: %v", err)
|
||||
}
|
||||
|
||||
workspace := t.TempDir()
|
||||
home := filepath.Join(workspace, "home")
|
||||
if err := os.MkdirAll(home, 0o755); err != nil {
|
||||
t.Fatalf("create fixture home: %v", err)
|
||||
}
|
||||
writeFile(t, filepath.Join(workspace, "go.mod"), "module example.com/first-agent-cli-fixture\n\ngo 1.24\n\nrequire go-micro.dev/v6 v6.0.0\n\nreplace go-micro.dev/v6 => "+filepath.ToSlash(absRoot)+"\n")
|
||||
writeFile(t, filepath.Join(workspace, "main.go"), firstAgentCLIFixtureSource())
|
||||
runInWorkspace(t, workspace, "go", "mod", "tidy")
|
||||
|
||||
micro := buildMicroBinary(t, absRoot)
|
||||
fixtureBin := filepath.Join(workspace, "first-agent-cli-fixture")
|
||||
runInWorkspace(t, workspace, "go", "build", "-o", fixtureBin, ".")
|
||||
fixture := exec.Command(fixtureBin)
|
||||
fixture.Dir = workspace
|
||||
fixture.Env = append(os.Environ(),
|
||||
"HOME="+home,
|
||||
"MICRO_AI_API_KEY=",
|
||||
"OPENAI_API_KEY=",
|
||||
"ANTHROPIC_API_KEY=",
|
||||
"GEMINI_API_KEY=",
|
||||
)
|
||||
var fixtureOut lockedBuffer
|
||||
fixture.Stdout = &fixtureOut
|
||||
fixture.Stderr = &fixtureOut
|
||||
if err := fixture.Start(); err != nil {
|
||||
t.Fatalf("start first-agent CLI fixture: %v\n%s", err, fixtureOut.String())
|
||||
}
|
||||
defer func() {
|
||||
if fixture.Process != nil {
|
||||
_ = fixture.Process.Signal(os.Interrupt)
|
||||
_ = fixture.Process.Kill()
|
||||
}
|
||||
}()
|
||||
|
||||
waitForCLIOutput(t, &fixtureOut, "first-agent fixture ready", 15*time.Second)
|
||||
waitForRegisteredAgent(t, micro, home, "assistant", &fixtureOut, 15*time.Second)
|
||||
|
||||
chat := runMicroCLIWithHome(t, micro, home, "chat", "--prompt", "Summarize my first-agent next steps", "assistant")
|
||||
for _, want := range []string{"assistant:", "install the CLI", "run a service", "chat with an agent"} {
|
||||
if !strings.Contains(chat, want) {
|
||||
t.Fatalf("micro chat assistant output missing %q:\n%s\nfixture output:\n%s", want, chat, fixtureOut.String())
|
||||
}
|
||||
}
|
||||
|
||||
stopFixture(t, fixture)
|
||||
|
||||
inspect := runMicroCLIWithHome(t, micro, home, "inspect", "agent", "assistant", "--limit", "1")
|
||||
for _, want := range []string{`Agent "assistant" runs`, "status=done", "last=done"} {
|
||||
if !strings.Contains(inspect, want) {
|
||||
t.Fatalf("micro inspect agent assistant output missing %q:\n%s\nfixture output:\n%s", want, inspect, fixtureOut.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func stopFixture(t *testing.T, cmd *exec.Cmd) {
|
||||
t.Helper()
|
||||
if cmd.Process == nil {
|
||||
return
|
||||
}
|
||||
done := make(chan error, 1)
|
||||
_ = cmd.Process.Signal(os.Interrupt)
|
||||
go func() { done <- cmd.Wait() }()
|
||||
select {
|
||||
case <-done:
|
||||
case <-time.After(2 * time.Second):
|
||||
_ = cmd.Process.Kill()
|
||||
<-done
|
||||
}
|
||||
}
|
||||
|
||||
func waitForCLIOutput(t *testing.T, out *lockedBuffer, want string, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
for time.Now().Before(deadline) {
|
||||
if strings.Contains(out.String(), want) {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("timed out waiting for fixture output %q; got:\n%s", want, out.String())
|
||||
}
|
||||
|
||||
func waitForRegisteredAgent(t *testing.T, micro, home, agent string, fixtureOut *lockedBuffer, timeout time.Duration) {
|
||||
t.Helper()
|
||||
deadline := time.Now().Add(timeout)
|
||||
var lastOut []byte
|
||||
var lastErr error
|
||||
for time.Now().Before(deadline) {
|
||||
cmd := exec.Command(micro, "agent", "list")
|
||||
cmd.Env = microCLIEnv(home)
|
||||
lastOut, lastErr = cmd.CombinedOutput()
|
||||
if lastErr == nil && strings.Contains(string(lastOut), agent) {
|
||||
return
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
t.Fatalf("timed out waiting for registered agent %q; last micro agent list error: %v\nlast output:\n%s\nfixture output:\n%s", agent, lastErr, lastOut, fixtureOut.String())
|
||||
}
|
||||
|
||||
type lockedBuffer struct {
|
||||
mu sync.Mutex
|
||||
b strings.Builder
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) Write(p []byte) (int, error) {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.b.Write(p)
|
||||
}
|
||||
|
||||
func (b *lockedBuffer) String() string {
|
||||
b.mu.Lock()
|
||||
defer b.mu.Unlock()
|
||||
return b.b.String()
|
||||
}
|
||||
|
||||
func firstAgentCLIFixtureSource() string {
|
||||
return `package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/service"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
type NotesService struct{}
|
||||
type ListNotesRequest struct{}
|
||||
type ListNotesResponse struct { Notes []string ` + "`json:\"notes\" description:\"Notes the assistant can summarize\"`" + ` }
|
||||
|
||||
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
|
||||
rsp.Notes = []string{"Install the CLI", "Run a service", "Chat with an agent"}
|
||||
return nil
|
||||
}
|
||||
|
||||
type mockModel struct{ opts ai.Options }
|
||||
func newMock(opts ...ai.Option) ai.Model { m := &mockModel{}; _ = m.Init(opts...); return m }
|
||||
func (m *mockModel) Init(opts ...ai.Option) error { for _, o := range opts { o(&m.opts) }; return nil }
|
||||
func (m *mockModel) Options() ai.Options { return m.opts }
|
||||
func (m *mockModel) String() string { return "first-agent-cli-fixture" }
|
||||
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) { return nil, fmt.Errorf("stream unsupported") }
|
||||
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
|
||||
for _, tool := range req.Tools { if strings.Contains(tool.Name, "List") && m.opts.ToolHandler != nil { m.opts.ToolHandler(ctx, ai.ToolCall{ID:"list-notes", Name: tool.Name, Input: map[string]any{}}); break } }
|
||||
return &ai.Response{Answer: "assistant: your first agent should install the CLI, run a service, then chat with an agent."}, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
ai.Register("first-agent-cli-fixture", newMock)
|
||||
home, _ := os.UserHomeDir()
|
||||
st := store.NewFileStore(store.DirOption(filepath.Join(home, "micro", "store")))
|
||||
defer st.Close()
|
||||
|
||||
svc := service.New(service.Name("notes"), service.Address("127.0.0.1:0"))
|
||||
if err := svc.Handle(&NotesService{}); err != nil { panic(err) }
|
||||
go func() { if err := svc.Run(); err != nil { fmt.Println(err); os.Exit(1) } }()
|
||||
defer svc.Server().Stop()
|
||||
|
||||
a := agent.New(agent.Name("assistant"), agent.Address("127.0.0.1:0"), agent.Services("notes"), agent.Provider("first-agent-cli-fixture"), agent.WithStore(st))
|
||||
go func() { if err := a.Run(); err != nil { fmt.Println(err); os.Exit(1) } }()
|
||||
defer a.Stop()
|
||||
|
||||
fmt.Println("first-agent fixture ready")
|
||||
stop := make(chan os.Signal, 1)
|
||||
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
|
||||
<-stop
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
func seedNoSecretAgentDebuggingState(t *testing.T, st store.Store) {
|
||||
t.Helper()
|
||||
scoped := store.Scope(st, "agent", "assistant")
|
||||
@@ -1232,22 +938,18 @@ func buildMicroBinary(t *testing.T, root string) string {
|
||||
func runMicroCLIWithHome(t *testing.T, micro, home string, args ...string) string {
|
||||
t.Helper()
|
||||
cmd := exec.Command(micro, args...)
|
||||
cmd.Env = microCLIEnv(home)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func microCLIEnv(home string) []string {
|
||||
return append(os.Environ(),
|
||||
cmd.Env = append(os.Environ(),
|
||||
"HOME="+home,
|
||||
"MICRO_AI_API_KEY=",
|
||||
"OPENAI_API_KEY=",
|
||||
"ANTHROPIC_API_KEY=",
|
||||
"GEMINI_API_KEY=",
|
||||
)
|
||||
out, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("micro %s failed: %v\n%s", strings.Join(args, " "), err, out)
|
||||
}
|
||||
return string(out)
|
||||
}
|
||||
|
||||
func TestFirstAgentWayfindingTargetsExist(t *testing.T) {
|
||||
@@ -1302,43 +1004,6 @@ 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, ".") {
|
||||
|
||||
@@ -25,7 +25,7 @@ run_step "run/chat/inspect: first-agent CLI boundaries" \
|
||||
run_step "deploy dry-run: configured target plan" \
|
||||
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
|
||||
run_step "chat/inspect: no-secret first-agent transcript and docs" \
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestFirstAgentCLIChatInspectFixture|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestZeroToHeroDeployDryRunCommandSmoke|TestYourFirstAgentTutorialSmoke' -count=1
|
||||
go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscript|TestNoSecretFirstAgentDebuggingSmoke|TestZeroToHeroReferenceDocs|TestZeroToHeroDeployDryRunCommandSmoke|TestYourFirstAgentTutorialSmoke' -count=1
|
||||
|
||||
# Deterministic no-secret reference scenarios. These use the real Go Micro
|
||||
# runtime and mock only the LLM provider. The support example is the maintained
|
||||
@@ -34,6 +34,6 @@ run_step "chat/inspect: no-secret first-agent transcript and docs" \
|
||||
run_step "first-agent app: runnable provider-free example" \
|
||||
go test ./examples/first-agent -run TestRunFirstAgent -count=1
|
||||
run_step "0→hero app: support lifecycle smoke" \
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
|
||||
run_step "flow history: deterministic services → agents → workflows harnesses" \
|
||||
go test ./internal/harness/universe ./internal/harness/plan-delegate -run 'Test.*Harness|TestPlanDelegateEndToEnd|TestPlanDelegateFlowHandoff' -count=1
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
---
|
||||
layout: blog
|
||||
title: "What's New in Go Micro: v6.6.0"
|
||||
permalink: /blog/35
|
||||
description: "Go Micro v6.6.0 strengthens the first-agent on-ramp, hardens plan/delegate recovery, and adds CI-backed security checks."
|
||||
---
|
||||
|
||||
# What's New in Go Micro: v6.6.0
|
||||
|
||||
*July 11, 2026 • By the Go Micro Team*
|
||||
|
||||
Go Micro v6.6.0 is a harness reliability release. The theme is the same services → agents → workflows lifecycle, but with more of the first-agent path covered by local checks and more recovery paths made deterministic when agents resume, retry, delegate, or notify.
|
||||
|
||||
## The first-agent path is harder to break
|
||||
|
||||
The README, website docs, examples, and 0→hero guide chain now have more harness coverage. The CLI and docs keep the path from install troubleshooting through `micro agent demo`, quickcheck, examples, the smallest first-agent, debugging, and 0→hero in one consistent order.
|
||||
|
||||
That matters because the first agent is where framework promises become real: scaffold a service, run it, chat with an agent, inspect what happened, and then graduate to workflows.
|
||||
|
||||
## Plan/delegate recovery is steadier
|
||||
|
||||
Plan/delegate got another reliability pass across side effects, notification replay, plan-only actions, timeout completion, and completed-plan recovery. These fixes are not flashy, but they are exactly the kind of harness work that keeps an agent from repeating work or losing state when a multi-step run is interrupted.
|
||||
|
||||
## Provider fallback keeps improving
|
||||
|
||||
AtlasCloud fallback handling now recovers more awkward provider outputs, including workspace repair calls, empty-argument text tool calls, spoken notification replays, and A2A fallback artifact text. The goal is pragmatic interop: when a provider response is close enough to a safe tool call, the harness should repair it; when it is not, the failure should stay visible.
|
||||
|
||||
## Security checks joined the loop
|
||||
|
||||
CI now includes a govulncheck gate and the autonomous loop can route vulnerability failures into triage. The release also includes toolchain and dependency updates for reachable CVEs, keeping the service framework side of the harness current while agent features continue to land.
|
||||
|
||||
## Read the changelog
|
||||
|
||||
The full release notes are in the [CHANGELOG](https://github.com/micro/go-micro/blob/master/CHANGELOG.md).
|
||||
|
||||
---
|
||||
|
||||
*Go Micro is an open source agent harness and service framework for Go. [Star us on GitHub](https://github.com/micro/go-micro).*
|
||||
|
||||
<div class="post-nav">
|
||||
<div><a href="/blog/34">← What's New in Go Micro: v6.3.15</a></div>
|
||||
<div><a href="/blog/">All Posts</a></div>
|
||||
</div>
|
||||
@@ -11,13 +11,6 @@ permalink: /blog/
|
||||
|
||||
<div class="posts">
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/35">What's New in Go Micro: v6.6.0</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">July 11, 2026</p>
|
||||
<p>Go Micro v6.6.0 strengthens the first-agent on-ramp, hardens plan/delegate recovery, improves provider fallback repair, and adds CI-backed security checks.</p>
|
||||
<a href="/blog/35">Read more →</a>
|
||||
</article>
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/34">What's New in Go Micro: v6.3.15</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">July 5, 2026</p>
|
||||
|
||||
@@ -104,25 +104,16 @@ If you are new, follow the architecture in the same order the runtime composes i
|
||||
CLI, `PATH`, version, and no-secret smoke path are healthy.
|
||||
2. [`micro agent demo`](getting-started.html#first-agent-on-ramp) — print the
|
||||
provider-free first-agent command and next docs steps from the installed CLI.
|
||||
3. `micro agent quickcheck` (or `micro agent debug`) — print the short recovery
|
||||
map when scaffold → run → chat → inspect stalls.
|
||||
4. `micro examples` — list the maintained provider-free runnable examples in
|
||||
copy/paste order.
|
||||
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle
|
||||
harness and runnable examples.
|
||||
6. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md)
|
||||
— choose the smallest no-secret first-agent, support reference, and interop
|
||||
examples from one map.
|
||||
7. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)
|
||||
3. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)
|
||||
— run one service-backed agent with a mock model.
|
||||
8. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — see the
|
||||
4. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — see the
|
||||
maintained support-agent path work without a provider key.
|
||||
9. [Your First Agent](guides/your-first-agent.html) — build and chat with a
|
||||
5. [Your First Agent](guides/your-first-agent.html) — build and chat with a
|
||||
service-backed agent.
|
||||
10. [Debugging your agent](guides/debugging-agents.html) — inspect service
|
||||
registration, tools, memory, providers, and run history.
|
||||
11. [0→hero Reference](guides/zero-to-hero.html) — walk scaffold → run → chat →
|
||||
inspect → flow → deploy dry-run as the maintained lifecycle contract.
|
||||
6. [Debugging your agent](guides/debugging-agents.html) — inspect service
|
||||
registration, tools, memory, providers, and run history.
|
||||
7. [0→hero Reference](guides/zero-to-hero.html) — walk scaffold → run → chat →
|
||||
inspect → flow → deploy dry-run as the maintained lifecycle contract.
|
||||
|
||||
## Related
|
||||
|
||||
|
||||
@@ -56,19 +56,15 @@ That install → scaffold → run → call loop is the 0→1 contract. It requir
|
||||
After this quick start, follow the agent path in order:
|
||||
|
||||
1. [Install troubleshooting](guides/install-troubleshooting.html) — verify the CLI install before agent work.
|
||||
|
||||
Run `make docs-wayfinding` to verify the focused no-secret docs/CLI contract that keeps these website and README commands aligned with the installed CLI.
|
||||
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
|
||||
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
6. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support), and next interop examples from one map.
|
||||
7. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) — run one service-backed agent with a mock model and no provider key.
|
||||
8. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — run a useful support agent with a mock model before setting up a provider key.
|
||||
9. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
|
||||
10. [Debugging your agent](guides/debugging-agents.html) — use `micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`, then `micro chat` and `micro inspect agent <name>` to recover service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
|
||||
11. [0→hero reference path](guides/zero-to-hero.html) — prove the full scaffold → run → chat → inspect → deploy dry-run lifecycle with commands exercised by `make harness`.
|
||||
3. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
4. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
5. [Examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support), and next interop examples from one map.
|
||||
6. [Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) — run one service-backed agent with a mock model and no provider key.
|
||||
7. [No-secret first-agent transcript](guides/no-secret-first-agent.html) — run a useful support agent with a mock model before setting up a provider key.
|
||||
8. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
|
||||
9. [Debugging your agent](guides/debugging-agents.html) — use `micro inspect agent <name>` to inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
|
||||
10. [0→hero reference path](guides/zero-to-hero.html) — prove the full scaffold → run → chat → inspect → deploy dry-run lifecycle with commands exercised by `make harness`.
|
||||
|
||||
## Write a Service
|
||||
|
||||
|
||||
@@ -24,11 +24,10 @@ Use the recovery command that matches where you are in the first-agent journey:
|
||||
| Checkpoint | When to use it | Command |
|
||||
| --- | --- | --- |
|
||||
| Install troubleshooting | `micro` is not installed, not on `PATH`, or the shell cannot run it. | [Install troubleshooting](install-troubleshooting.html) |
|
||||
| Quick recovery map | The first-agent loop stalled and you want the short scaffold → run → chat → inspect checklist before reading this full guide. | `micro agent quickcheck` (alias: `micro agent debug`) |
|
||||
| Preflight before `micro run` | You have not started the local runtime yet and want to verify Go, CLI, provider-key, and gateway-port prerequisites. | `micro agent preflight` |
|
||||
| Doctor after `micro run` | `micro run` is active, but chat, the `/agent` gateway, agent registration, provider settings, or inspect/run history is not behaving. | `micro agent doctor` |
|
||||
|
||||
`micro agent quickcheck` is the quickest breadcrumb when you are unsure where the first-agent path failed: it prints the preflight, run, doctor, inspect, and no-secret fallback commands in one place. `micro agent preflight` is read-only and runs before the first local run; failed
|
||||
`micro agent preflight` is read-only and runs before the first local run; failed
|
||||
checks include `Fix:` and `Next:` lines for Go, CLI installation, provider-key
|
||||
setup, and the local gateway port. Once `micro run` is already up, switch to
|
||||
`micro agent doctor` so the recovery output follows the live gateway, chat
|
||||
|
||||
@@ -50,29 +50,6 @@ 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,14 +18,14 @@ 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, 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 wayfinding | README and the website getting-started docs keep the no-secret → first-agent → debugging → 0→hero links present and in order. | `go test ./internal/harness/zero-to-hero-ci -run TestFirstAgentWayfindingDocs -count=1` |
|
||||
| First agent | `micro new`, `micro agent preflight`, `micro run`, `micro chat`, and `micro inspect agent <name>` stay available for the documented first-agent walkthrough. | `go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1` |
|
||||
| Run | `micro run` remains the local development entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
| Chat | `micro chat` remains the interactive agent entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
| Inspect | `micro inspect agent <name>`, `micro agent history <name>`, `micro inspect flow <flow>`, and `micro flow runs <flow>` remain discoverable for run history; the no-secret debugging smoke seeds durable agent history and runs the documented inspect/history commands without provider keys. | `go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1` |
|
||||
| Deploy | `micro deploy --dry-run prod` resolves the documented deploy target without touching remote infrastructure. | `go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunCommandSmoke -count=1` |
|
||||
| Smallest first agent | `examples/first-agent` runs one service-backed agent with a deterministic mock model and no provider key. | `go test ./examples/first-agent -run TestRunFirstAgent -count=1` |
|
||||
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1` |
|
||||
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1` |
|
||||
| Runtime harnesses | Real services, agents, durable flows, store-backed history, delegation, and A2A run with only the model mocked. | `./internal/harness/zero-to-hero-ci/run.sh` and `make provider-conformance-mock` |
|
||||
|
||||
## Find the one-command entrypoint
|
||||
@@ -96,7 +96,7 @@ go test ./internal/harness/zero-to-hero-ci -run TestZeroToHeroDeployDryRunComman
|
||||
go test ./examples/first-agent -run TestRunFirstAgent -count=1
|
||||
|
||||
# Maintained 0→hero support-desk reference app.
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle|TestZeroToHeroInspectTranscript' -count=1
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
|
||||
|
||||
# Durable services → agents → workflows reference scenarios.
|
||||
./internal/harness/zero-to-hero-ci/run.sh
|
||||
|
||||
@@ -16,7 +16,7 @@ It's built on a pluggable architecture of Go interfaces: service discovery, clie
|
||||
|
||||
## Learn More
|
||||
|
||||
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent) for the fastest provider-free run, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build a service-backed agent and talk to it with `micro chat`, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
|
||||
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.
|
||||
|
||||
Otherwise continue to read the docs for more information about the framework.
|
||||
|
||||
@@ -27,10 +27,9 @@ 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 and talk to it with `micro chat`
|
||||
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent end to end
|
||||
- [MCP & AI Agents](mcp.html) - Turn services into AI-callable tools with the Model Context Protocol
|
||||
- [CLI & Gateway Guide](guides/cli-gateway.html) - Development vs Production modes
|
||||
- [`micro loop` quickstart](guides/micro-loop.html) - Scaffold an autonomous CI-gated improvement loop
|
||||
@@ -55,7 +54,7 @@ Otherwise continue to read the docs for more information about the framework.
|
||||
|
||||
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → `micro inspect agent <name>` → deploy dry-run with CI-backed commands
|
||||
- [No-secret first-agent transcript](guides/no-secret-first-agent.html) - Run the first useful agent path without a provider key
|
||||
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent and talk to it with `micro chat`
|
||||
- [Your First Agent](guides/your-first-agent.html) - Build a service-backed agent end to end
|
||||
- [Building AI-Native Services](guides/ai-native-services.html) - End-to-end tutorial for MCP-enabled services
|
||||
- [MCP Security Guide](guides/mcp-security.html) - Auth, scopes, rate limiting, and audit logging
|
||||
- [Tool Description Best Practices](guides/tool-descriptions.html) - Writing docs that make agents effective
|
||||
|
||||
+3
-3
@@ -143,13 +143,13 @@ func (m *memoryModel) Delete(ctx context.Context, key string, v interface{}) err
|
||||
func (m *memoryModel) List(ctx context.Context, result interface{}, opts ...QueryOption) error {
|
||||
// result must be *[]*T
|
||||
rv := reflect.ValueOf(result)
|
||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
||||
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("model: result must be a pointer to a slice")
|
||||
}
|
||||
sliceVal := rv.Elem()
|
||||
elemType := sliceVal.Type().Elem() // *T
|
||||
structType := elemType
|
||||
if structType.Kind() == reflect.Pointer {
|
||||
if structType.Kind() == reflect.Ptr {
|
||||
structType = structType.Elem()
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ func (m *memoryModel) List(ctx context.Context, result interface{}, opts ...Quer
|
||||
for i, row := range rows {
|
||||
vp := reflect.New(structType)
|
||||
MapToStruct(s, row, vp.Interface())
|
||||
if elemType.Kind() == reflect.Pointer {
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
results.Index(i).Set(vp)
|
||||
} else {
|
||||
results.Index(i).Set(vp.Elem())
|
||||
|
||||
@@ -166,13 +166,13 @@ func (d *postgresModel) Delete(ctx context.Context, key string, v interface{}) e
|
||||
|
||||
func (d *postgresModel) List(ctx context.Context, result interface{}, opts ...model.QueryOption) error {
|
||||
rv := reflect.ValueOf(result)
|
||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
||||
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("model/postgres: result must be a pointer to a slice")
|
||||
}
|
||||
sliceVal := rv.Elem()
|
||||
elemType := sliceVal.Type().Elem()
|
||||
structType := elemType
|
||||
if structType.Kind() == reflect.Pointer {
|
||||
if structType.Kind() == reflect.Ptr {
|
||||
structType = structType.Elem()
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func (d *postgresModel) List(ctx context.Context, result interface{}, opts ...mo
|
||||
for i, fields := range fieldMaps {
|
||||
vp := reflect.New(structType)
|
||||
model.MapToStruct(schema, fields, vp.Interface())
|
||||
if elemType.Kind() == reflect.Pointer {
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
results.Index(i).Set(vp)
|
||||
} else {
|
||||
results.Index(i).Set(vp.Elem())
|
||||
|
||||
+4
-4
@@ -33,7 +33,7 @@ type Field struct {
|
||||
// BuildSchema extracts a Schema from a struct type using reflection.
|
||||
func BuildSchema(v interface{}, opts ...RegisterOption) *Schema {
|
||||
t := reflect.TypeOf(v)
|
||||
if t.Kind() == reflect.Pointer {
|
||||
if t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func BuildSchema(v interface{}, opts ...RegisterOption) *Schema {
|
||||
// StructToMap converts a struct pointer to a map of column name → value.
|
||||
func StructToMap(schema *Schema, v interface{}) map[string]any {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() == reflect.Pointer {
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
fields := make(map[string]any, len(schema.Fields))
|
||||
@@ -116,7 +116,7 @@ func StructToMap(schema *Schema, v interface{}) map[string]any {
|
||||
// MapToStruct fills a struct pointer from a map of column name → value.
|
||||
func MapToStruct(schema *Schema, fields map[string]any, v interface{}) {
|
||||
rv := reflect.ValueOf(v)
|
||||
if rv.Kind() == reflect.Pointer {
|
||||
if rv.Kind() == reflect.Ptr {
|
||||
rv = rv.Elem()
|
||||
}
|
||||
for _, f := range schema.Fields {
|
||||
@@ -155,7 +155,7 @@ func KeyValue(schema *Schema, v interface{}) string {
|
||||
// ResolveType returns the struct reflect.Type from a value (handles pointers and slices).
|
||||
func ResolveType(v interface{}) reflect.Type {
|
||||
t := reflect.TypeOf(v)
|
||||
for t.Kind() == reflect.Pointer || t.Kind() == reflect.Slice {
|
||||
for t.Kind() == reflect.Ptr || t.Kind() == reflect.Slice {
|
||||
t = t.Elem()
|
||||
}
|
||||
return t
|
||||
|
||||
@@ -167,13 +167,13 @@ func (d *sqliteModel) Delete(ctx context.Context, key string, v interface{}) err
|
||||
func (d *sqliteModel) List(ctx context.Context, result interface{}, opts ...model.QueryOption) error {
|
||||
// result must be *[]*T
|
||||
rv := reflect.ValueOf(result)
|
||||
if rv.Kind() != reflect.Pointer || rv.Elem().Kind() != reflect.Slice {
|
||||
if rv.Kind() != reflect.Ptr || rv.Elem().Kind() != reflect.Slice {
|
||||
return fmt.Errorf("model/sqlite: result must be a pointer to a slice")
|
||||
}
|
||||
sliceVal := rv.Elem()
|
||||
elemType := sliceVal.Type().Elem()
|
||||
structType := elemType
|
||||
if structType.Kind() == reflect.Pointer {
|
||||
if structType.Kind() == reflect.Ptr {
|
||||
structType = structType.Elem()
|
||||
}
|
||||
|
||||
@@ -226,7 +226,7 @@ func (d *sqliteModel) List(ctx context.Context, result interface{}, opts ...mode
|
||||
for i, fields := range fieldMaps {
|
||||
vp := reflect.New(structType)
|
||||
model.MapToStruct(schema, fields, vp.Interface())
|
||||
if elemType.Kind() == reflect.Pointer {
|
||||
if elemType.Kind() == reflect.Ptr {
|
||||
results.Index(i).Set(vp)
|
||||
} else {
|
||||
results.Index(i).Set(vp.Elem())
|
||||
|
||||
+2
-2
@@ -48,7 +48,7 @@ func extractMethodDoc(method reflect.Method, rcvrType reflect.Type) (description
|
||||
|
||||
// Find the receiver type name (e.g., "Users" from *Users)
|
||||
rcvrTypeName := rcvrType.Name()
|
||||
if rcvrTypeName == "" && rcvrType.Kind() == reflect.Pointer {
|
||||
if rcvrTypeName == "" && rcvrType.Kind() == reflect.Ptr {
|
||||
rcvrTypeName = rcvrType.Elem().Name()
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ func extractHandlerDocs(handler interface{}) map[string]map[string]string {
|
||||
|
||||
// Get the receiver type for methods
|
||||
rcvrType := typ
|
||||
if rcvrType.Kind() == reflect.Pointer {
|
||||
if rcvrType.Kind() == reflect.Ptr {
|
||||
rcvrType = rcvrType.Elem()
|
||||
}
|
||||
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestExtractHandlerDocs(t *testing.T) {
|
||||
}
|
||||
|
||||
// Test NoDoc (should have no metadata or only empty metadata)
|
||||
if len(docs["NoDoc"]) > 0 {
|
||||
if docs["NoDoc"] != nil && len(docs["NoDoc"]) > 0 {
|
||||
t.Logf("NoDoc metadata: %+v", docs["NoDoc"])
|
||||
// Check if all values are empty
|
||||
allEmpty := true
|
||||
|
||||
+2
-2
@@ -16,7 +16,7 @@ func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
@@ -62,7 +62,7 @@ func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
}
|
||||
case reflect.Slice:
|
||||
p := v.Elem()
|
||||
if p.Kind() == reflect.Pointer {
|
||||
if p.Kind() == reflect.Ptr {
|
||||
p = p.Elem()
|
||||
}
|
||||
arg.Type = "[]" + p.Name()
|
||||
|
||||
@@ -16,7 +16,7 @@ func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
return nil
|
||||
}
|
||||
|
||||
if v.Kind() == reflect.Pointer {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ func extractValue(v reflect.Type, d int) *registry.Value {
|
||||
}
|
||||
case reflect.Slice:
|
||||
p := v.Elem()
|
||||
if p.Kind() == reflect.Pointer {
|
||||
if p.Kind() == reflect.Ptr {
|
||||
p = p.Elem()
|
||||
}
|
||||
arg.Type = "[]" + p.Name()
|
||||
|
||||
+1
-1
@@ -367,7 +367,7 @@ func (g *grpcServer) processRequest(stream grpc.ServerStream, service *service,
|
||||
|
||||
// Decode the argument value.
|
||||
argIsValue := false // if true, need to indirect before calling.
|
||||
if mtype.ArgType.Kind() == reflect.Pointer {
|
||||
if mtype.ArgType.Kind() == reflect.Ptr {
|
||||
argv = reflect.New(mtype.ArgType.Elem())
|
||||
} else {
|
||||
argv = reflect.New(mtype.ArgType)
|
||||
|
||||
@@ -54,7 +54,7 @@ func isExported(name string) bool {
|
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
@@ -107,7 +107,7 @@ func prepareEndpoint(method reflect.Method, log logger.Logger) *methodType {
|
||||
return nil
|
||||
}
|
||||
|
||||
if replyType.Kind() != reflect.Pointer {
|
||||
if replyType.Kind() != reflect.Ptr {
|
||||
log.Logf(logger.ErrorLevel, "method %v reply type not a pointer: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -207,7 +207,7 @@ func (g *grpcServer) createSubHandler(sb *subscriber, opts server.Options) broke
|
||||
var isVal bool
|
||||
var req reflect.Value
|
||||
|
||||
if handler.reqType.Kind() == reflect.Pointer {
|
||||
if handler.reqType.Kind() == reflect.Ptr {
|
||||
req = reflect.New(handler.reqType.Elem())
|
||||
} else {
|
||||
req = reflect.New(handler.reqType)
|
||||
|
||||
@@ -107,7 +107,7 @@ func isExported(name string) bool {
|
||||
|
||||
// Is this type exported or a builtin?
|
||||
func isExportedOrBuiltinType(t reflect.Type) bool {
|
||||
for t.Kind() == reflect.Pointer {
|
||||
for t.Kind() == reflect.Ptr {
|
||||
t = t.Elem()
|
||||
}
|
||||
// PkgPath will be non-empty even for an exported type,
|
||||
@@ -160,7 +160,7 @@ func prepareMethod(method reflect.Method, logger log.Logger) *methodType {
|
||||
return nil
|
||||
}
|
||||
|
||||
if replyType.Kind() != reflect.Pointer {
|
||||
if replyType.Kind() != reflect.Ptr {
|
||||
logger.Logf(log.ErrorLevel, "method %v reply type not a pointer: %v", mname, replyType)
|
||||
return nil
|
||||
}
|
||||
@@ -376,7 +376,7 @@ func (router *router) readRequest(r Request) (service *service, mtype *methodTyp
|
||||
|
||||
// Decode the argument value.
|
||||
argIsValue := false // if true, need to indirect before calling.
|
||||
if mtype.ArgType.Kind() == reflect.Pointer {
|
||||
if mtype.ArgType.Kind() == reflect.Ptr {
|
||||
argv = reflect.New(mtype.ArgType.Elem())
|
||||
} else {
|
||||
argv = reflect.New(mtype.ArgType)
|
||||
@@ -576,7 +576,7 @@ func (router *router) ProcessMessage(ctx context.Context, subscriber string, msg
|
||||
var req reflect.Value
|
||||
|
||||
// check whether the handler is a pointer
|
||||
if handler.reqType.Kind() == reflect.Pointer {
|
||||
if handler.reqType.Kind() == reflect.Ptr {
|
||||
req = reflect.New(handler.reqType.Elem())
|
||||
} else {
|
||||
req = reflect.New(handler.reqType)
|
||||
|
||||
Reference in New Issue
Block a user