Compare commits

..

1 Commits

Author SHA1 Message Date
Codex d837151e41 docs: refresh planner priorities for 4403
Harness (E2E) / Harnesses (mock LLM) (push) Waiting to run
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Waiting to run
Lint / golangci-lint (push) Waiting to run
Run Tests / Unit Tests (push) Waiting to run
Run Tests / Etcd Integration Tests (push) Waiting to run
2026-07-08 22:34:34 +00:00
42 changed files with 64 additions and 796 deletions
+2 -2
View File
@@ -21,8 +21,8 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Fix AtlasCloud plan-delegate incomplete delegate tool call** ([#4487](https://github.com/micro/go-micro/issues/4487)) — The adoption-facing inner-loop contract is now shipped, and there are no open `codex` PRs in flight, so the top remaining gap returns to a Now-phase resilience failure in the lived services → agents → workflows story: AtlasCloud can create task side effects, then stop on an incomplete repaired `delegate` tool call before notification/delegation completes. This is higher value than a single marker wording failure because it protects the cross-runtime handoff after real side effects and keeps the 0→hero/plan-delegate harness operable across providers.
2. **Fix AtlasCloud agent harness missing conformance marker** ([#4486](https://github.com/micro/go-micro/issues/4486)) — Once the delegate repair path is stable, close the remaining scheduled live-conformance gap where AtlasCloud's agent harness exhausts retries without emitting the required marker. This is still a Now-phase provider-conformance item, but narrower: it verifies the core agent/model+tool contract rather than the full services → agents → workflows handoff.
1. **Resume agent runs from checkpoints** ([#4368](https://github.com/micro/go-micro/issues/4368)) — #4391 closed the OpenTelemetry RunInfo gap, #4397 documented resume checkpoint limits, and #4399/#4402 moved the getting-started contract into CI, so the highest-value remaining depth seam is a focused, non-breaking durable-agent resume slice that preserves completed tool calls and avoids duplicate side effects before broader durability or API design work.
2. **Broaden provider streaming conformance** ([#4386](https://github.com/micro/go-micro/issues/4386)) — The blog says Anthropic streaming shipped, but the roadmap still calls for provider-backed streaming across chat and A2A. Add a focused, provider-gated conformance slice so streaming stays end-to-end rather than becoming a one-provider success story.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
-28
View File
@@ -17,41 +17,13 @@ below is kept current between tags and rolled into the next version when it ship
## [Unreleased]
### Added
- **Agent stream provider conformance** — provider conformance now covers agent streaming behavior so streaming-capable providers stay aligned with the harness contract. (`agent/`, `internal/harness/`)
### Changed
- **Provider model call timeouts** — model call timeout enforcement now wraps provider calls more defensively, reducing hangs in agent and harness paths. (`agent/`, `ai/`)
- **First-agent harness diagnostics** — getting-started harness logs now make first-run and 0→hero failures easier to locate. (`internal/harness/`)
### Fixed
- **Partial text tool calls** — text tool-call recovery now repairs partial function-style calls more reliably before fallback parsing continues. (`agent/`)
- **Retry timeout test stability** — retry timeout coverage is less race-prone. (`agent/`)
- **Checkpointed tool-call resume** — resumed agent runs now preserve checkpointed tool calls across startup resume paths. (`agent/`)
### Documentation
- **Roadmap agent status** — public roadmap docs now reflect the current agent lifecycle status more consistently. (`internal/website/docs/`)
- **Agent resume limits** — docs now describe checkpoint resume boundaries for agent runs. (`internal/website/docs/`)
---
## [6.4.0] - July 2026
### Added
- **Provider HTTP retry signals** — provider failures now preserve HTTP status and `Retry-After` details so retry classification and backoff can respond to rate limits and unavailable providers. (`ai/`)
- **Zero-to-hero deploy dry-run verification** — the maintained 0→hero harness now covers deploy dry-run boundaries for the services → agents → workflows lifecycle. (`internal/harness/`)
- **First-agent CLI wayfinding verification** — the harness now checks that first-agent CLI wayfinding stays discoverable. (`internal/harness/`)
- **Agent startup resume verification** — agent startup resume now has focused checkpoint coverage. (`agent/`, `internal/harness/`)
- **Direct first-agent chat prompts** — first-agent flows can accept direct chat prompts, reducing friction in the first useful conversation. (`cmd/micro/`, `agent/`)
- **Workflow run info on tool spans** — agent tool spans now include workflow run details for easier trace correlation. (`agent/`, `flow/`)
### Fixed
- **Stream fallback memory** — unsupported streaming attempts no longer leave stale duplicate user turns before fallback paths continue with non-streaming agent calls. (`agent/`)
- **Function-style text tool calls** — agent fallback parsing now recognizes provider replies that render tools as function-style calls, including nested JSON arguments. (`agent/`)
- **Plan/delegate notify recovery** — plan-delegate recovery now waits for recovered notify side effects and routes retries through the communications agent that owns the notification. (`internal/harness/`)
- **Onboarding side-effect enforcement** — the agent-flow harness now fails when required onboarding side effects are missing, making lifecycle regressions visible. (`internal/harness/`)
- **Plan/delegate notify stability** — notify recovery is more deterministic across retry and replay paths. (`agent/`, `internal/harness/`)
- **AtlasCloud MiniMax tool fallback** — AtlasCloud MiniMax service-tool fallback now handles 400 responses and follow-up retries more reliably. (`ai/atlascloud/`, `agent/`)
### Documentation
- **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/`)
+2 -13
View File
@@ -8,7 +8,7 @@ LDFLAGS = -X $(GIT_IMPORT).BuildDate=$(BUILD_DATE) -X $(GIT_IMPORT).GitCommit=$(
# GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser-cross:v1.25.7
GORELEASER_DOCKER_IMAGE = ghcr.io/goreleaser/goreleaser:latest
.PHONY: test test-race test-coverage harness inner-loop cli-wayfinding docs-wayfinding install-smoke provider-conformance-mock provider-conformance lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker
.PHONY: test test-race test-coverage harness cli-wayfinding docs-wayfinding install-smoke provider-conformance-mock provider-conformance lint fmt install-tools proto clean help gorelease-dry-run gorelease-dry-run-docker
# Default target
help:
@@ -19,7 +19,6 @@ help:
@echo " make test-coverage - Run tests with coverage"
@echo " make lint - Run linter"
@echo " make harness - Run deterministic getting-started and end-to-end harnesses"
@echo " make inner-loop - Verify scaffold → run/chat/inspect → deploy dry-run contract"
@echo " make cli-wayfinding - Verify installed first-agent CLI wayfinding commands"
@echo " make docs-wayfinding - Verify first-agent docs wayfinding links resolve locally"
@echo " make install-smoke - Verify the local install.sh and first-run CLI smoke path"
@@ -53,21 +52,11 @@ test-coverage:
# run/chat/inspect, and 0→hero regressions before a PR is opened.
harness:
$(MAKE) cli-wayfinding
$(MAKE) inner-loop
go test ./cmd/micro/cli/new -run TestZeroToOne -count=1
./internal/harness/zero-to-hero-ci/run.sh
go run ./internal/harness/agent-flow
$(MAKE) provider-conformance-mock
# Focused provider-free CLI inner-loop contract: scaffold a service, keep the
# run/chat/inspect commands discoverable, and prove deploy dry-run reaches the
# documented boundary without remote side effects. Use this when README/docs/CLI
# drift is the concern and the full runtime harness is more than you need.
inner-loop:
go test ./cmd/micro/cli/new -run TestZeroToOne -count=1
go test ./cmd/micro -run 'TestFirstAgentWalkthroughCLIBoundaries|TestZeroToHeroCLIBoundaries|TestZeroToHeroCommandPrintsMaintainedNoSecretPath' -count=1
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
go test ./internal/harness/zero-to-hero-ci -run 'TestZeroToHeroDeployDryRunCommandSmoke|TestNoSecretFirstAgentDebuggingSmoke|TestYourFirstAgentTutorialSmoke' -count=1
# Verify the installed CLI keeps the first-agent on-ramp commands discoverable.
# This guards the no-secret commands README/docs recommend (`micro agent demo`,
# `micro examples`, and `micro zero-to-hero`) as a CI contract.
-6
View File
@@ -78,12 +78,6 @@ access or provider keys, use:
make install-smoke
```
To verify the focused CLI inner-loop contract — scaffold → run/chat/inspect → deploy dry-run — use:
```bash
make inner-loop
```
To run the broader local contract (including the [0→hero services → agents → workflows path](internal/website/docs/guides/zero-to-hero.md),
chat/inspect CLI boundaries, and deploy dry-run), use:
+4 -18
View File
@@ -14,9 +14,8 @@ The full, current roadmap lives at **[go-micro.dev/docs/roadmap](https://go-micr
## Where we are (v6)
Services, agents (`plan`/`delegate`, guardrails, memory, tool middleware,
checkpoint/resume, and OpenTelemetry run spans), durable flows, the MCP and A2A
gateways (both directions, including A2A streaming,
Services, agents (`plan`/`delegate`, guardrails, memory, tool middleware), durable
flows, the MCP and A2A gateways (both directions, including A2A streaming,
push notifications, and multi-turn continuation), x402 paid tools, secure by
default.
@@ -40,24 +39,11 @@ default.
propagation, retry/backoff.
- **Getting-started contract** — define and CI-verify the 0→1 and 0→hero flows.
## Shipped agent depth
- **Durable agent loop** — opt-in `Checkpoint` support lets agent `Ask` and
streaming runs persist, list pending work, and resume without replaying completed
tool calls. Human-input pauses resume through explicit input helpers.
- **Agent observability** — agent `RunInfo` now feeds OpenTelemetry spans/events
across runs, model turns, tool calls, retries, delegation lineage, and resume
checkpoints.
## Next — agentic depth
- **Durable agent loop** — resume a long run via `Checkpoint` (flows already do).
- **Streaming** — broaden provider-backed `ai.Stream` coverage and keep chat/A2A streaming end to end.
- **Resume operations polish** — keep improving CLI/docs breadcrumbs for finding
pending agent runs and deciding whether to call resume, resume-input, or stream
resume in production.
- **Observability hardening** — keep span attributes and run inspection coherent
across agents, flows, and gateways as more providers and workflow paths are
exercised.
- **Agent observability** — `RunInfo` → OpenTelemetry spans.
## Later
+1 -15
View File
@@ -33,7 +33,6 @@ import (
_ "go-micro.dev/v6/ai/atlascloud"
_ "go-micro.dev/v6/ai/gemini"
_ "go-micro.dev/v6/ai/groq"
_ "go-micro.dev/v6/ai/minimax"
_ "go-micro.dev/v6/ai/mistral"
_ "go-micro.dev/v6/ai/ollama"
_ "go-micro.dev/v6/ai/openai"
@@ -415,15 +414,6 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
continue
}
}
if toolName := partialTextToolCallName(resp.Reply, toolList); len(resp.ToolCalls) == 0 && toolName != "" && planCompletionTurn < maxPlanCompletionTurns {
if resp.Reply != "" {
a.mem.Add("assistant", resp.Reply)
}
message = fmt.Sprintf("Your previous response started a %q tool call but did not finish valid tool-call markup or JSON arguments, so no tool was executed. Retry the same step now by emitting one complete valid tool call for %q. Do not describe the action in prose, and do not claim completion until the tool call succeeds.", toolName, toolName)
a.mem.Add("user", message)
messages = a.mem.Messages()
continue
}
break
}
@@ -442,13 +432,9 @@ func (a *agentImpl) askLocked(ctx context.Context, runID, message, parentRunID s
reply += resp.Answer
}
completedToolCalls := checkpointToolCalls(run.Steps)
if a.currentRun != nil {
completedToolCalls = checkpointToolCalls(a.currentRun.Steps)
}
res := &Response{
Reply: reply,
ToolCalls: mergeCheckpointToolCalls(completedToolCalls, resp.ToolCalls),
ToolCalls: resp.ToolCalls,
Agent: a.opts.Name,
RunID: a.runID,
ParentID: parentRunID,
-10
View File
@@ -38,16 +38,6 @@ func TestNew(t *testing.T) {
}
}
func TestBundledProviderImportsIncludeMiniMaxForConformance(t *testing.T) {
if model := ai.New("minimax", ai.WithAPIKey("test-key")); model == nil {
t.Fatal("ai.New(\"minimax\") returned nil; agent live conformance cannot exercise MiniMax")
}
caps := ai.ProviderCapabilities("minimax")
if !caps.Stream || !caps.ToolStream {
t.Fatalf("MiniMax capabilities = %#v, want streaming and tool streaming registered", caps)
}
}
func TestChatResponseIncludesRunIDs(t *testing.T) {
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
return &ai.Response{Reply: "ok"}, nil
-54
View File
@@ -4,7 +4,6 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"go-micro.dev/v6/ai"
@@ -289,56 +288,3 @@ func upsertStep(steps *[]flow.StepRecord, rec flow.StepRecord) int {
*steps = append(*steps, rec)
return len(*steps) - 1
}
func checkpointToolCalls(steps []flow.StepRecord) []ai.ToolCall {
calls := make([]ai.ToolCall, 0, len(steps))
for _, step := range steps {
call, ok := checkpointToolCall(step)
if !ok {
continue
}
calls = append(calls, call)
}
return calls
}
func checkpointToolCall(step flow.StepRecord) (ai.ToolCall, bool) {
if step.Status != "done" || !strings.HasPrefix(step.Name, "tool:") {
return ai.ToolCall{}, false
}
parts := strings.SplitN(strings.TrimPrefix(step.Name, "tool:"), ":", 2)
if len(parts) != 2 || parts[0] == "" {
return ai.ToolCall{}, false
}
input := map[string]any{}
if parts[1] != "null" && parts[1] != "" {
if err := json.Unmarshal([]byte(parts[1]), &input); err != nil {
return ai.ToolCall{}, false
}
}
return ai.ToolCall{Name: parts[0], Input: input, Result: step.Result}, true
}
func mergeCheckpointToolCalls(checkpointed, current []ai.ToolCall) []ai.ToolCall {
if len(checkpointed) == 0 {
return current
}
seen := make(map[string]struct{}, len(current))
for _, call := range current {
seen[toolCallKey(call.Name, call.Input)] = struct{}{}
}
merged := make([]ai.ToolCall, 0, len(checkpointed)+len(current))
for _, call := range checkpointed {
if _, ok := seen[toolCallKey(call.Name, call.Input)]; ok {
continue
}
merged = append(merged, call)
}
merged = append(merged, current...)
return merged
}
func toolCallKey(name string, input map[string]any) string {
b, _ := json.Marshal(input)
return name + ":" + string(b)
}
-6
View File
@@ -104,12 +104,6 @@ func TestResumeFailedCheckpointDoesNotReplayCompletedTool(t *testing.T) {
if resp.Reply != "finished from checkpoint" {
t.Fatalf("Resume reply = %q", resp.Reply)
}
if len(resp.ToolCalls) != 1 || resp.ToolCalls[0].Name != "external.charge" || resp.ToolCalls[0].Result != "charged" {
t.Fatalf("resumed tool calls = %#v, want preserved completed charge call", resp.ToolCalls)
}
if got := resp.ToolCalls[0].Input["order"]; got != "42" {
t.Fatalf("resumed tool input order = %#v, want 42", got)
}
if toolRuns != 1 {
t.Fatalf("tool executions after Resume = %d, want completed tool was not replayed", toolRuns)
}
-170
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
@@ -46,120 +45,6 @@ func TestAgentProviderConformanceMatrix(t *testing.T) {
}
}
func TestAgentProviderStreamConformanceMatrix(t *testing.T) {
providers := []conformanceProvider{
{name: "fake"},
{name: "openai", key: "OPENAI_API_KEY", model: "GO_MICRO_CONFORMANCE_OPENAI_MODEL", live: true},
{name: "anthropic", key: "ANTHROPIC_API_KEY", model: "GO_MICRO_CONFORMANCE_ANTHROPIC_MODEL", live: true},
{name: "atlascloud", key: "ATLASCLOUD_API_KEY", model: "GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL", live: true},
{name: "groq", key: "GROQ_API_KEY", model: "GO_MICRO_CONFORMANCE_GROQ_MODEL", live: true},
{name: "minimax", key: "MINIMAX_API_KEY", model: "GO_MICRO_CONFORMANCE_MINIMAX_MODEL", live: true},
{name: "mistral", key: "MISTRAL_API_KEY", model: "GO_MICRO_CONFORMANCE_MISTRAL_MODEL", live: true},
{name: "together", key: "TOGETHER_API_KEY", model: "GO_MICRO_CONFORMANCE_TOGETHER_MODEL", live: true},
}
selected := selectedConformanceProviders(os.Getenv("GO_MICRO_AGENT_CONFORMANCE_PROVIDERS"))
for _, provider := range providers {
provider := provider
if len(selected) > 0 && !selected[provider.name] {
continue
}
t.Run(provider.name, func(t *testing.T) {
runAgentStreamConformanceScenario(t, provider)
})
}
}
func runAgentStreamConformanceScenario(t *testing.T, provider conformanceProvider) {
t.Helper()
if provider.live {
if os.Getenv(provider.key) == "" {
t.Skipf("%s not set; skipping live %s stream conformance", provider.key, provider.name)
}
if os.Getenv("GO_MICRO_AGENT_CONFORMANCE_LIVE") == "" {
t.Skipf("GO_MICRO_AGENT_CONFORMANCE_LIVE not set; skipping live %s stream conformance", provider.name)
}
caps := ai.ProviderCapabilities(provider.name)
if !caps.Stream {
t.Fatalf("ProviderCapabilities(%q).Stream = false, want true for stream conformance", provider.name)
}
if !caps.ToolStream {
t.Skipf("ProviderCapabilities(%q).ToolStream = false; skipping live tool stream conformance", provider.name)
}
} else {
var sawToolSchema bool
fakeStream = func(ctx context.Context, opts ai.Options, req *ai.Request) (ai.Stream, error) {
if req.Prompt != "Stream exactly: agent-stream-conformance-ok" {
return nil, fmt.Errorf("prompt = %q", req.Prompt)
}
if len(req.Messages) == 0 || req.Messages[len(req.Messages)-1].Role != "user" || req.Messages[len(req.Messages)-1].Content != req.Prompt {
return nil, fmt.Errorf("messages = %#v, want current user turn", req.Messages)
}
for _, tool := range req.Tools {
if tool.Name == "conformance_echo" {
sawToolSchema = true
}
}
if !sawToolSchema {
return nil, errors.New("stream request omitted conformance tool schema")
}
return &sliceStream{chunks: []string{"agent-stream-", "conformance-ok"}}, nil
}
defer func() { fakeStream = nil }()
}
agentOpts := []Option{
Name("stream-conformance-" + provider.name),
Provider(provider.name),
APIKey(os.Getenv(provider.key)),
Prompt("Stream conformance: preserve the exact requested marker in the final answer."),
WithRegistry(registry.NewMemoryRegistry()),
WithStore(store.NewMemoryStore()),
WithMemory(NewInMemory(8)),
ModelCallTimeout(45 * time.Second),
WithTool("conformance_echo", "Echo a conformance value and return a deterministic marker.", map[string]any{
"value": map[string]any{"type": "string", "description": "value to echo"},
}, func(ctx context.Context, input map[string]any) (string, error) {
return `{"marker":"agent-stream-conformance-ok"}`, nil
}),
}
if provider.model != "" {
if model := os.Getenv(provider.model); model != "" {
agentOpts = append(agentOpts, Model(model))
}
}
stream, err := New(agentOpts...).Stream(context.Background(), "Stream exactly: agent-stream-conformance-ok")
if err != nil {
t.Fatalf("Stream: %v", err)
}
defer stream.Close()
var reply strings.Builder
deadline := time.After(45 * time.Second)
for {
select {
case <-deadline:
t.Fatal("timed out waiting for streamed final output")
default:
}
chunk, err := stream.Recv()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
t.Fatalf("Recv: %v", err)
}
reply.WriteString(chunk.Reply)
if strings.Contains(reply.String(), "agent-stream-conformance-ok") {
return
}
}
if got := reply.String(); !strings.Contains(got, "agent-stream-conformance-ok") {
t.Fatalf("streamed reply %q does not include conformance marker", got)
}
}
func selectedConformanceProviders(csv string) map[string]bool {
out := map[string]bool{}
for _, part := range strings.Split(csv, ",") {
@@ -746,61 +631,6 @@ func TestAgentExecutesProviderTextToolCallFallback(t *testing.T) {
}
}
func TestAgentRepairsPartialTextToolCallFallback(t *testing.T) {
attempts := 0
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
if opts.ToolHandler == nil {
return nil, errors.New("missing tool handler")
}
attempts++
if attempts == 1 {
return &ai.Response{Reply: `<tool_call name="conformance_echo">`}, nil
}
if !strings.Contains(req.Prompt, "did not finish valid tool-call markup") {
return nil, fmt.Errorf("repair prompt = %q, want partial tool-call repair guidance", req.Prompt)
}
return &ai.Response{
Reply: `<tool_call name="conformance_echo">{"value":"agent-conformance"}</tool_call>`,
}, nil
}
defer func() { fakeGen = nil }()
var sawTool bool
a := New(
Name("conformance-partial-text-tool"),
Provider("fake"),
WithRegistry(registry.NewMemoryRegistry()),
WithStore(store.NewMemoryStore()),
WithMemory(NewInMemory(4)),
WithTool("conformance_echo", "Echo a conformance value.", map[string]any{
"value": map[string]any{"type": "string"},
}, func(ctx context.Context, input map[string]any) (string, error) {
sawTool = true
if input["value"] != "agent-conformance" {
return "", fmt.Errorf("unexpected value %v", input["value"])
}
return `{"marker":"agent-conformance-ok"}`, nil
}),
)
resp, err := a.Ask(context.Background(), "Run the partial text tool call fallback.")
if err != nil {
t.Fatalf("Ask: %v", err)
}
if attempts != 2 {
t.Fatalf("attempts = %d, want repair retry", attempts)
}
if !sawTool {
t.Fatal("repaired text tool call fallback did not execute the tool")
}
if len(resp.ToolCalls) != 1 || resp.ToolCalls[0].Name != "conformance_echo" {
t.Fatalf("ToolCalls = %+v, want conformance_echo", resp.ToolCalls)
}
if !strings.Contains(resp.Reply, "agent-conformance-ok") {
t.Fatalf("Reply = %q, want tool result marker", resp.Reply)
}
}
func TestAgentExecutesTextToolCallFallbackAfterStructuredToolCall(t *testing.T) {
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
if opts.ToolHandler == nil {
-2
View File
@@ -68,8 +68,6 @@ func init() {
_ = m.Init(opts...)
return m
})
ai.RegisterStream("fake")
ai.RegisterToolStream("fake")
}
// fakeClient embeds the default client (so NewRequest works) and
-49
View File
@@ -240,55 +240,6 @@ func TestAskCancellationDuringToolCallFailsRun(t *testing.T) {
}
}
func TestSlowProviderTimeoutPreventsLateToolSideEffects(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
done := make(chan struct{})
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
close(started)
<-release
defer close(done)
if opts.ToolHandler == nil {
t.Fatal("missing tool handler")
}
res := opts.ToolHandler(ctx, ai.ToolCall{ID: "late-1", Name: "external.create", Input: map[string]any{"title": "too late"}})
if !strings.Contains(res.Content, context.DeadlineExceeded.Error()) {
t.Errorf("late tool result = %q, want deadline exceeded", res.Content)
}
return &ai.Response{Reply: "late", ToolCalls: []ai.ToolCall{{ID: "late-1", Name: "external.create", Input: map[string]any{"title": "too late"}, Result: res.Content}}}, nil
}
defer func() { fakeGen = nil }()
toolRuns := 0
a := newTestAgent(
Name("slow-provider-late-tool"),
ModelCallTimeout(10*time.Millisecond),
WithTool("external.create", "create once", nil, func(context.Context, map[string]any) (string, error) {
toolRuns++
return "created", nil
}),
)
_, err := a.Ask(context.Background(), "provider times out before tool")
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("Ask error = %v, want deadline exceeded", err)
}
select {
case <-started:
default:
t.Fatal("provider was not called")
}
close(release)
select {
case <-done:
case <-time.After(time.Second):
t.Fatal("late provider call did not finish")
}
if toolRuns != 0 {
t.Fatalf("late tool executions = %d, want 0", toolRuns)
}
}
func TestAskCheckpointRecordsTerminalOperationalFailureStatus(t *testing.T) {
tests := []struct {
name string
-32
View File
@@ -15,7 +15,6 @@ var fencedJSONBlock = regexp.MustCompile("(?s)```(?:json)?\\s*(.*?)\\s*```")
var taggedToolCallBlock = regexp.MustCompile(`(?s)<[^<>]*(?:tool_call|tool_calls|function=)[^<>]*>(.*?)</[^<>]*>`)
var singleTaggedToolCall = regexp.MustCompile(`(?s)<(tool_call\b[^<>]*|[^<>]*function\s*=[^<>]*)>(.*?)</[^<>]*>`)
var taggedToolNameAttr = regexp.MustCompile(`(?i)(?:function|name|tool)\s*=\s*["\']?([^"\'\s>]+)`)
var openingTaggedToolCall = regexp.MustCompile(`(?i)<(tool_call\b[^<>]*|[^<>]*function\s*=[^<>]*)>`)
type textToolCall struct {
ID string `json:"id"`
@@ -114,37 +113,6 @@ func parseTextToolCalls(text string, tools []ai.Tool) []ai.ToolCall {
return nil
}
func partialTextToolCallName(text string, tools []ai.Tool) string {
text = html.UnescapeString(text)
allowed := textToolNames(tools)
if len(allowed) == 0 {
return ""
}
openMatches := openingTaggedToolCall.FindAllStringSubmatchIndex(text, -1)
if len(openMatches) == 0 {
return ""
}
closedMatches := singleTaggedToolCall.FindAllStringSubmatchIndex(text, -1)
for _, open := range openMatches {
closed := false
for _, match := range closedMatches {
if match[0] == open[0] {
closed = true
break
}
}
if closed {
continue
}
tag := text[open[2]:open[3]]
name := taggedToolName(tag)
if canonical := allowed[name]; canonical != "" {
return canonical
}
}
return ""
}
func textToolNames(tools []ai.Tool) map[string]string {
allowed := map[string]string{}
for _, tool := range tools {
-1
View File
@@ -19,7 +19,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("anthropic")
ai.RegisterToolStream("anthropic")
}
// Provider implements the ai.Model interface for Anthropic Claude
-64
View File
@@ -139,33 +139,6 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen
}
}
if toolName := atlascloudPartialTextToolCallName(resp.Reply, req.Tools); toolName != "" {
repairReq := map[string]any{
"model": p.opts.Model,
"messages": append(append([]map[string]any(nil), messages...),
map[string]any{"role": "assistant", "content": resp.Reply},
map[string]any{"role": "user", "content": fmt.Sprintf("Your previous response started a %q tool call but did not finish valid tool-call markup or JSON arguments, so no tool was executed. Retry the same step now by emitting one complete valid tool call for %q. Do not describe the action in prose, and do not claim completion until the tool call succeeds.", toolName, toolName)},
),
}
if p.opts.MaxTokens > 0 {
repairReq["max_tokens"] = p.opts.MaxTokens
}
if len(tools) > 0 {
repairReq["tools"] = tools
}
resp, rawMessage, err = p.callAPI(ctx, "chat-partial-tool-repair", repairReq)
if err != nil {
return nil, fmt.Errorf("atlascloud partial text tool-call repair failed for %q: %w", toolName, err)
}
if atlascloudPartialTextToolCallName(resp.Reply, req.Tools) != "" && len(resp.ToolCalls) == 0 {
if toolName == "plan" {
resp.Reply = atlascloudPlanFallbackTextToolCall(req.Prompt)
} else {
return nil, fmt.Errorf("atlascloud returned incomplete text tool call for %q after repair", toolName)
}
}
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
@@ -506,43 +479,6 @@ func atlascloudToolCallsText(calls any) string {
return string(b)
}
func atlascloudPartialTextToolCallName(text string, tools []ai.Tool) string {
if !strings.Contains(text, "<tool_call") {
return ""
}
if strings.Contains(text, "</tool_call>") {
return ""
}
for _, tool := range tools {
for _, name := range []string{tool.Name, tool.OriginalName} {
if name == "" {
continue
}
if strings.Contains(text, `name="`+name+`"`) || strings.Contains(text, `name='`+name+`'`) {
return tool.Name
}
}
}
return ""
}
func atlascloudPlanFallbackTextToolCall(prompt string) string {
task := strings.TrimSpace(prompt)
if task == "" {
task = "continue the requested work"
}
args, err := json.Marshal(map[string]any{
"steps": []map[string]string{{
"task": task,
"status": "pending",
}},
})
if err != nil {
return `<tool_call name="plan">{"steps":[{"task":"continue the requested work","status":"pending"}]}</tool_call>`
}
return `<tool_call name="plan">` + string(args) + `</tool_call>`
}
func atlascloudMinimaxCompatTools(model string, input []ai.Tool) ([]map[string]any, string) {
if !atlascloudIsMinimaxModel(model) || len(input) == 0 {
return nil, ""
-99
View File
@@ -520,105 +520,6 @@ func TestProvider_GeneratePreservesFollowUpTextToolCallInReply(t *testing.T) {
}
}
func TestProvider_GenerateRepairsInitialPartialTextToolCall(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
var body map[string]any
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
t.Fatalf("decode request: %v", err)
}
bodies = append(bodies, body)
w.Header().Set("Content-Type", "application/json")
switch len(bodies) {
case 1:
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"plan\">"}}]}`))
case 2:
messages := body["messages"].([]any)
last := messages[len(messages)-1].(map[string]any)
if last["role"] != "user" || !strings.Contains(last["content"].(string), "did not finish valid tool-call markup") {
t.Fatalf("repair prompt = %#v, want partial tool-call guidance", last)
}
if _, ok := body["tools"]; !ok {
t.Fatalf("repair request did not keep tools available: %#v", body)
}
_, _ = w.Write([]byte(`{"choices":[{"message":{"content":"<tool_call name=\"plan\">{\"steps\":[{\"task\":\"create tasks\"}]}</tool_call>"}}]}`))
default:
t.Fatalf("unexpected API call %d", len(bodies))
}
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
resp, err := p.Generate(context.Background(), &ai.Request{
Prompt: "plan and delegate",
Tools: []ai.Tool{
{Name: "plan", Description: "record a plan", Properties: map[string]any{"steps": map[string]any{"type": "array"}}},
{Name: "delegate", Description: "delegate work", Properties: map[string]any{"task": map[string]any{"type": "string"}}},
},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
if !strings.Contains(resp.Reply, `<tool_call name="plan">`) || !strings.Contains(resp.Reply, `</tool_call>`) {
t.Fatalf("Reply = %q, want completed text tool call", resp.Reply)
}
if len(bodies) != 2 {
t.Fatalf("requests = %d, want initial plus repair", len(bodies))
}
}
func TestProvider_GenerateFallsBackAfterRepeatedPartialPlanTextToolCall(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=\"plan\">"}}]}`))
}))
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: "plan and delegate",
Tools: []ai.Tool{{Name: "plan", Description: "record a plan"}},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
if !strings.Contains(resp.Reply, `<tool_call name="plan">`) || !strings.Contains(resp.Reply, `</tool_call>`) {
t.Fatalf("Reply = %q, want completed fallback plan text tool call", resp.Reply)
}
if !strings.Contains(resp.Reply, "plan and delegate") {
t.Fatalf("Reply = %q, want fallback plan seeded from prompt", resp.Reply)
}
}
func TestProvider_GenerateErrorsAfterRepeatedPartialNonPlanTextToolCall(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=\"delegate\">"}}]}`))
}))
defer ts.Close()
p := NewProvider(
ai.WithAPIKey("test-key"),
ai.WithBaseURL(ts.URL),
ai.WithModel("minimaxai/minimax-m3"),
)
_, err := p.Generate(context.Background(), &ai.Request{
Prompt: "plan and delegate",
Tools: []ai.Tool{{Name: "delegate", Description: "delegate work"}},
})
if err == nil || !strings.Contains(err.Error(), "incomplete text tool call") {
t.Fatalf("Generate error = %v, want incomplete text tool call error", err)
}
}
func TestProvider_GenerateRetriesMinimaxBuiltInsAsTextTools(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+5 -24
View File
@@ -23,10 +23,6 @@ type Capabilities struct {
// Providers that only satisfy the Model interface with ErrStreamingUnsupported
// leave this false until their Stream implementation is usable.
Stream bool `json:"stream"`
// ToolStream reports whether the provider supports agent Stream requests that
// include tool schemas. Providers may support plain token streaming while
// leaving this false when their streaming API cannot accept tools.
ToolStream bool `json:"tool_stream"`
}
// ProviderCapabilities reports the capabilities registered for provider.
@@ -35,14 +31,12 @@ func ProviderCapabilities(provider string) Capabilities {
_, hasImage := imageProviders[provider]
_, hasVideo := videoProviders[provider]
_, hasStream := streamProviders[provider]
_, hasToolStream := toolStreamProviders[provider]
return Capabilities{
Model: hasModel,
Image: hasImage,
Video: hasVideo,
Stream: hasStream,
ToolStream: hasToolStream,
Model: hasModel,
Image: hasImage,
Video: hasVideo,
Stream: hasStream,
}
}
@@ -64,9 +58,6 @@ func CapabilityMatrix() map[string]Capabilities {
for name := range streamProviders {
names[name] = struct{}{}
}
for name := range toolStreamProviders {
names[name] = struct{}{}
}
matrix := make(map[string]Capabilities, len(names))
for name := range names {
@@ -97,18 +88,10 @@ func RegisterStream(provider string) {
streamProviders[provider] = struct{}{}
}
// RegisterToolStream records that provider can accept tool schemas in Stream
// requests. This is intentionally separate from RegisterStream because some
// providers can stream tokens but cannot expose tools while streaming.
func RegisterToolStream(provider string) {
toolStreamProviders[provider] = struct{}{}
}
var streamProviders = make(map[string]struct{})
var toolStreamProviders = make(map[string]struct{})
// RegisteredProviders returns the registered provider names in sorted order.
// kind may be "model", "image", "video", "stream", "tool_stream", or empty for the union of all
// kind may be "model", "image", "video", "stream", or empty for the union of all
// provider registries.
func RegisteredProviders(kind string) []string {
names := map[string]struct{}{}
@@ -138,8 +121,6 @@ func RegisteredProviders(kind string) []string {
add(providers)
case "stream":
add(streamProviders)
case "tool_stream":
add(toolStreamProviders)
case "image":
add(imageProviders)
case "video":
+7 -21
View File
@@ -44,14 +44,14 @@ func TestRegisteredProviders(t *testing.T) {
func TestCapabilityRows(t *testing.T) {
got := ai.CapabilityRows()
want := []ai.CapabilityRow{
{Provider: "anthropic", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: true}},
{Provider: "anthropic", Capabilities: ai.Capabilities{Model: true, Stream: true}},
{Provider: "atlascloud", Capabilities: ai.Capabilities{Model: true, Image: true, Video: true, Stream: true}},
{Provider: "gemini", Capabilities: ai.Capabilities{Model: true}},
{Provider: "groq", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: true}},
{Provider: "minimax", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: true}},
{Provider: "mistral", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: true}},
{Provider: "openai", Capabilities: ai.Capabilities{Model: true, Image: true, Stream: true, ToolStream: true}},
{Provider: "together", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: true}},
{Provider: "groq", Capabilities: ai.Capabilities{Model: true, Stream: true}},
{Provider: "minimax", Capabilities: ai.Capabilities{Model: true, Stream: true}},
{Provider: "mistral", Capabilities: ai.Capabilities{Model: true, Stream: true}},
{Provider: "openai", Capabilities: ai.Capabilities{Model: true, Image: true, Stream: true}},
{Provider: "together", Capabilities: ai.Capabilities{Model: true, Stream: true}},
}
if !reflect.DeepEqual(got, want) {
t.Fatalf("CapabilityRows() = %#v, want %#v", got, want)
@@ -71,7 +71,7 @@ func TestCapabilityMatrix(t *testing.T) {
}
}
if caps := ai.ProviderCapabilities("openai"); caps != (ai.Capabilities{Model: true, Image: true, Stream: true, ToolStream: true}) {
if caps := ai.ProviderCapabilities("openai"); caps != (ai.Capabilities{Model: true, Image: true, Stream: true}) {
t.Fatalf("ProviderCapabilities(openai) = %#v", caps)
}
if caps := ai.ProviderCapabilities("atlascloud"); caps != (ai.Capabilities{Model: true, Image: true, Video: true, Stream: true}) {
@@ -95,17 +95,3 @@ func TestRegisterStream(t *testing.T) {
t.Fatalf("RegisteredProviders(stream) = %#v, want %#v", got, want)
}
}
func TestRegisterToolStream(t *testing.T) {
ai.RegisterToolStream("test-tool-stream")
if caps := ai.ProviderCapabilities("test-tool-stream"); caps != (ai.Capabilities{ToolStream: true}) {
t.Fatalf("ProviderCapabilities(test-tool-stream) = %#v", caps)
}
got := ai.RegisteredProviders("tool_stream")
want := []string{"anthropic", "groq", "minimax", "mistral", "openai", "test-tool-stream", "together"}
if !reflect.DeepEqual(got, want) {
t.Fatalf("RegisteredProviders(tool_stream) = %#v, want %#v", got, want)
}
}
-1
View File
@@ -30,7 +30,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("groq")
ai.RegisterToolStream("groq")
}
type Provider struct {
-1
View File
@@ -30,7 +30,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("minimax")
ai.RegisterToolStream("minimax")
}
type Provider struct {
-1
View File
@@ -30,7 +30,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("mistral")
ai.RegisterToolStream("mistral")
}
type Provider struct {
-1
View File
@@ -45,7 +45,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("ollama")
ai.RegisterToolStream("ollama")
}
// Provider implements the ai.Model interface for Ollama.
-1
View File
@@ -22,7 +22,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("openai")
ai.RegisterToolStream("openai")
}
// Provider implements the ai.Model interface for OpenAI
+1 -22
View File
@@ -157,7 +157,7 @@ func GenerateWithRetry(ctx context.Context, m Model, req *Request, policy Genera
info.MaxAttempts = policy.MaxAttempts
callCtx = WithRunInfo(callCtx, info)
}
resp, err := generateAttempt(callCtx, m, req, opts...)
resp, err := m.Generate(callCtx, req, opts...)
cancel()
// Caller cancellation/deadline always wins and is not retried, even if
@@ -197,27 +197,6 @@ func GenerateWithRetry(ctx context.Context, m Model, req *Request, policy Genera
return nil, &RetryError{Attempts: policy.MaxAttempts, Kind: ClassifyError(last), Err: last}
}
func generateAttempt(ctx context.Context, m Model, req *Request, opts ...GenerateOption) (*Response, error) {
if err := ctx.Err(); err != nil {
return nil, err
}
type result struct {
resp *Response
err error
}
done := make(chan result, 1)
go func() {
resp, err := m.Generate(ctx, req, opts...)
done <- result{resp: resp, err: err}
}()
select {
case res := <-done:
return res.resp, res.err
case <-ctx.Done():
return nil, ctx.Err()
}
}
func retryBackoff(err error, attempt int, base time.Duration) time.Duration {
backoff := base
if backoff <= 0 {
+4 -33
View File
@@ -4,7 +4,6 @@ import (
"context"
"errors"
"net/http"
"sync/atomic"
"testing"
"time"
)
@@ -70,9 +69,9 @@ func TestGenerateWithRetryDoesNotRetryCallerCancellation(t *testing.T) {
}
func TestGenerateWithRetryHonorsPerAttemptTimeout(t *testing.T) {
var attempts atomic.Int32
attempts := 0
model := retryModel{generate: func(ctx context.Context, _ *Request, _ ...GenerateOption) (*Response, error) {
attempts.Add(1)
attempts++
<-ctx.Done()
return nil, ctx.Err()
}}
@@ -92,8 +91,8 @@ func TestGenerateWithRetryHonorsPerAttemptTimeout(t *testing.T) {
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("error = %v, want context.DeadlineExceeded", err)
}
if got := attempts.Load(); got != 2 {
t.Fatalf("attempts = %d, want 2", got)
if attempts != 2 {
t.Fatalf("attempts = %d, want 2", attempts)
}
}
@@ -136,34 +135,6 @@ func TestGenerateWithRetryAddsAttemptMetadataToRunInfo(t *testing.T) {
}
}
func TestGenerateWithRetryReturnsWhenProviderIgnoresTimeout(t *testing.T) {
started := make(chan struct{})
release := make(chan struct{})
model := retryModel{generate: func(ctx context.Context, req *Request, opts ...GenerateOption) (*Response, error) {
close(started)
<-release
return &Response{Reply: "late"}, nil
}}
defer close(release)
start := time.Now()
_, err := GenerateWithRetry(context.Background(), model, &Request{Prompt: "hi"}, GeneratePolicy{
Timeout: 10 * time.Millisecond,
MaxAttempts: 1,
})
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("GenerateWithRetry error = %v, want deadline exceeded", err)
}
if elapsed := time.Since(start); elapsed > 200*time.Millisecond {
t.Fatalf("GenerateWithRetry took %s after deadline, want prompt return", elapsed)
}
select {
case <-started:
default:
t.Fatal("provider was not called")
}
}
type statusErr int
func (e statusErr) Error() string { return "provider status" }
-1
View File
@@ -30,7 +30,6 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("together")
ai.RegisterToolStream("together")
}
type Provider struct {
+1 -2
View File
@@ -67,11 +67,10 @@ provider-free agent path:
```
micro agent demo
micro examples
micro zero-to-hero
```
Those commands point at the smallest mock-model first-agent example, the no-secret
transcript, and the 0→hero support app before you add provider-backed chat.
transcript, and the support app before you add provider-backed chat.
### Output
+7 -7
View File
@@ -31,7 +31,7 @@ func TestZeroToOneContract(t *testing.T) {
}
}
generated.assertLocalModule(t)
generated.replaceModule(t)
generated.build(t)
generated.run(t)
generated.call(t, "Alice", "Hello Alice")
@@ -52,7 +52,7 @@ func TestZeroToOneNoMCPContract(t *testing.T) {
t.Fatalf("--no-mcp generated main.go with MCP wiring:\n%s", main)
}
generated.assertLocalModule(t)
generated.replaceModule(t)
generated.build(t)
generated.run(t)
generated.call(t, "Bob", "Hello Bob")
@@ -112,7 +112,6 @@ func generateService(t *testing.T, name string, args ...string) generatedService
if err != nil {
t.Fatal(err)
}
t.Setenv("MICRO_NEW_GO_MICRO_REPLACE", repoRoot)
tmp := t.TempDir()
oldwd, err := os.Getwd()
@@ -143,7 +142,7 @@ func generateService(t *testing.T, name string, args ...string) generatedService
return generatedService{dir: filepath.Join(tmp, name), repoRoot: repoRoot}
}
func (g generatedService) assertLocalModule(t *testing.T) {
func (g generatedService) replaceModule(t *testing.T) {
t.Helper()
modPath := filepath.Join(g.dir, "go.mod")
@@ -151,9 +150,10 @@ func (g generatedService) assertLocalModule(t *testing.T) {
if err != nil {
t.Fatal(err)
}
want := "replace go-micro.dev/v6 => " + filepath.ToSlash(g.repoRoot)
if !strings.Contains(string(mod), want) {
t.Fatalf("generated go.mod missing local replace %q:\n%s", want, mod)
modText := strings.Replace(string(mod), "go-micro.dev/v6 latest", "go-micro.dev/v6 v6.0.0", 1)
modText += "\nreplace go-micro.dev/v6 => " + filepath.ToSlash(g.repoRoot) + "\n"
if err := os.WriteFile(modPath, []byte(modText), 0644); err != nil {
t.Fatal(err)
}
}
-7
View File
@@ -39,8 +39,6 @@ type config struct {
UseGoPath bool
// MicroVersion is the go-micro version to require in go.mod
MicroVersion string
// MicroReplace optionally points generated services at a local go-micro checkout.
MicroReplace string
// Files
Files []file
// Comments
@@ -71,10 +69,6 @@ func microVersion() string {
return "latest"
}
func microReplace() string {
return filepath.ToSlash(os.Getenv("MICRO_NEW_GO_MICRO_REPLACE"))
}
type file struct {
Path string
Tmpl string
@@ -221,7 +215,6 @@ func Run(ctx *cli.Context) error {
GoPath: goPath,
UseGoPath: false,
MicroVersion: microVersion(),
MicroReplace: microReplace(),
}
if useProto {
+2 -6
View File
@@ -10,9 +10,7 @@ require (
github.com/golang/protobuf latest
google.golang.org/protobuf latest
)
{{if .MicroReplace}}
replace go-micro.dev/v6 => {{.MicroReplace}}
{{end}}`
`
// ModuleNoProto is the default go.mod: no protobuf dependencies.
// MicroVersion is the version this CLI was built from (or "latest"), so a
@@ -22,7 +20,5 @@ replace go-micro.dev/v6 => {{.MicroReplace}}
go 1.23
require go-micro.dev/v6 {{.MicroVersion}}
{{if .MicroReplace}}
replace go-micro.dev/v6 => {{.MicroReplace}}
{{end}}`
`
)
-1
View File
@@ -17,7 +17,6 @@ provider API keys you want to exercise:
| Atlas Cloud | `ATLASCLOUD_API_KEY` | `GO_MICRO_CONFORMANCE_ATLASCLOUD_MODEL` |
| Gemini | `GEMINI_API_KEY` | `GO_MICRO_CONFORMANCE_GEMINI_MODEL` |
| Groq | `GROQ_API_KEY` | `GO_MICRO_CONFORMANCE_GROQ_MODEL` |
| MiniMax | `MINIMAX_API_KEY` | `GO_MICRO_CONFORMANCE_MINIMAX_MODEL` |
| Mistral | `MISTRAL_API_KEY` | `GO_MICRO_CONFORMANCE_MISTRAL_MODEL` |
| Together | `TOGETHER_API_KEY` | `GO_MICRO_CONFORMANCE_TOGETHER_MODEL` |
@@ -206,10 +206,10 @@ func writeSummaryMarkdown(path string, summary conformanceSummary) error {
func capabilityMarkdown(rows []ai.CapabilityRow) string {
var b strings.Builder
b.WriteString("| Provider | Model | Image | Video | Streaming | Tool streaming |\n")
b.WriteString("| --- | --- | --- | --- | --- | --- |\n")
b.WriteString("| Provider | Model | Image | Video | Streaming |\n")
b.WriteString("| --- | --- | --- | --- | --- |\n")
for _, row := range rows {
fmt.Fprintf(&b, "| %s | %s | %s | %s | %s | %s |\n", row.Provider, mark(row.Model), mark(row.Image), mark(row.Video), mark(row.Stream), mark(row.ToolStream))
fmt.Fprintf(&b, "| %s | %s | %s | %s | %s |\n", row.Provider, mark(row.Model), mark(row.Image), mark(row.Video), mark(row.Stream))
}
return b.String()
}
@@ -250,9 +250,9 @@ func mark(ok bool) string {
func printCapabilityMatrix() {
fmt.Println("Provider capability matrix:")
fmt.Println("provider model image video stream tool-stream")
fmt.Println("provider model image video stream")
for _, row := range ai.CapabilityRows() {
fmt.Printf("%-12s %-5s %-5s %-5s %-6s %-11s\n", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream), yesNo(row.ToolStream))
fmt.Printf("%-12s %-5s %-5s %-5s %-6s\n", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream))
}
fmt.Println()
}
@@ -421,7 +421,7 @@ func runAgentConformance(provider string, timeout time.Duration) error {
if provider == "mock" {
testProvider = "fake"
}
cmd := exec.CommandContext(ctx, "go", "test", "./agent", "-run", "TestAgentProvider(ConformanceMatrix|StreamConformanceMatrix)", "-count=1", "-v")
cmd := exec.CommandContext(ctx, "go", "test", "./agent", "-run", "TestAgentProviderConformanceMatrix", "-count=1", "-v")
cmd.Dir = repoRoot()
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
@@ -93,7 +93,7 @@ func TestWriteCapabilityMarkdown(t *testing.T) {
}
got := string(b)
for _, want := range []string{
"| Provider | Model | Image | Video | Streaming | Tool streaming |",
"| Provider | Model | Image | Video | Streaming |",
"| mock | ✅ | — | — | — |",
"| vision | — | ✅ | ✅ | — |",
} {
@@ -20,7 +20,6 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "zero-to-hero.md"))
for _, want := range []string{
"make harness",
"make inner-loop",
"go test ./cmd/micro/cli/new -run TestZeroToOne -count=1",
"go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1",
"go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1",
@@ -51,27 +50,11 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
t.Fatalf("0→hero CI run script missing lifecycle command %q", want)
}
}
for _, want := range []string{
"scaffold:",
"run/chat/inspect:",
"deploy dry-run:",
"chat/inspect:",
"first-agent app:",
"0→hero app:",
"flow history:",
} {
if !strings.Contains(runScript, want) {
t.Fatalf("0→hero CI run script missing debuggable boundary label %q", want)
}
}
readme := readFile(t, filepath.Join(root, "README.md"))
if !strings.Contains(readme, "internal/website/docs/guides/zero-to-hero.md") {
t.Fatal("README does not point to the canonical 0→hero guide")
}
if !strings.Contains(readme, "make inner-loop") {
t.Fatal("README does not expose the focused CLI inner-loop contract")
}
nav := readFile(t, filepath.Join(root, "internal", "website", "_data", "navigation.yml"))
if !strings.Contains(nav, "0→hero Reference") || !strings.Contains(nav, "/docs/guides/zero-to-hero.html") {
@@ -289,12 +272,10 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
links: []string{
"internal/website/docs/guides/install-troubleshooting.md",
"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",
"internal/website/docs/guides/debugging-agents.md",
"micro inspect agent <name>",
"internal/website/docs/guides/zero-to-hero.md",
},
},
@@ -316,26 +297,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"./support/",
},
},
{
name: "repository examples wayfinding index",
file: filepath.Join(root, "examples", "INDEX.md"),
heading: "## Recommended adoption path",
links: []string{
"./hello-world/",
"./first-agent/",
"./support/",
},
},
{
name: "micro README first-agent on-ramp",
file: filepath.Join(root, "cmd", "micro", "README.md"),
heading: "## First agent on-ramp",
links: []string{
"micro agent demo",
"micro examples",
"micro zero-to-hero",
},
},
{
name: "website examples index",
file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"),
@@ -355,7 +316,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
links: []string{
"guides/install-troubleshooting.html",
"micro agent demo",
"micro examples",
"micro zero-to-hero",
"https://github.com/micro/go-micro/blob/master/examples/INDEX.md",
"https://github.com/micro/go-micro/tree/master/examples/support",
@@ -363,7 +323,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
},
},
@@ -374,7 +333,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
links: []string{
"guides/install-troubleshooting.html",
"micro agent demo",
"micro examples",
"micro zero-to-hero",
"https://github.com/micro/go-micro/blob/master/examples/INDEX.md",
"https://github.com/micro/go-micro/tree/master/examples/support",
@@ -382,7 +340,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
},
},
@@ -397,7 +354,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
},
},
@@ -444,11 +400,6 @@ func TestFirstAgentWayfindingLinkTargetsResolve(t *testing.T) {
file: filepath.Join(root, "examples", "README.md"),
heading: "## Recommended first-agent path",
},
{
name: "repository examples wayfinding index",
file: filepath.Join(root, "examples", "INDEX.md"),
heading: "## Recommended adoption path",
},
{
name: "website examples index",
file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"),
@@ -658,7 +609,6 @@ func TestGettingStartedDocsLeadWithNoSecretFirstRun(t *testing.T) {
"micro zero-to-hero",
"guides/no-secret-first-agent.html",
"guides/debugging-agents.html",
"micro inspect agent <name>",
"guides/zero-to-hero.html",
},
},
+1 -1
View File
@@ -35,5 +35,5 @@ 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' -count=1
run_step "flow history: deterministic services → agents → workflows harnesses" \
run_step "workflows: deterministic services → agents → workflows harnesses" \
go test ./internal/harness/universe ./internal/harness/plan-delegate -run 'Test.*Harness|TestPlanDelegateEndToEnd|TestPlanDelegateFlowHandoff' -count=1
+1 -1
View File
@@ -63,7 +63,7 @@ After this quick start, follow the agent path in order:
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.
9. [Debugging your agent](guides/debugging-agents.html) — inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent surprises you.
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
@@ -30,23 +30,23 @@ imports are linked in:
```go
for _, row := range ai.CapabilityRows() {
fmt.Printf("%s: chat=%t image=%t video=%t stream=%t tool_stream=%t\n", row.Provider, row.Model, row.Image, row.Video, row.Stream, row.ToolStream)
fmt.Printf("%s: chat=%t image=%t video=%t stream=%t\n", row.Provider, row.Model, row.Image, row.Video, row.Stream)
}
```
The built-in providers currently register these capability interfaces:
| Provider | Chat/text (`ai.Model`) | Image (`ai.ImageModel`) | Video (`ai.VideoModel`) | Streaming (`ai.Stream`) | Tool streaming |
| --- | --- | --- | --- | --- | --- |
| `anthropic` | Yes | No | No | Yes | Yes |
| `atlascloud` | Yes | Yes | Yes | Yes | No |
| `gemini` | Yes | No | No | No | No |
| `groq` | Yes | No | No | Yes | Yes |
| `minimax` | Yes | No | No | Yes | Yes |
| `mistral` | Yes | No | No | Yes | Yes |
| `ollama` | Yes | No | No | Yes | Yes |
| `openai` | Yes | Yes | No | Yes | Yes |
| `together` | Yes | No | No | Yes | Yes |
| Provider | Chat/text (`ai.Model`) | Image (`ai.ImageModel`) | Video (`ai.VideoModel`) | Streaming (`ai.Stream`) |
| --- | --- | --- | --- | --- |
| `anthropic` | Yes | No | No | Yes |
| `atlascloud` | Yes | Yes | Yes | Yes |
| `gemini` | Yes | No | No | No |
| `groq` | Yes | No | No | Yes |
| `minimax` | Yes | No | No | Yes |
| `mistral` | Yes | No | No | Yes |
| `ollama` | Yes | No | No | Yes |
| `openai` | Yes | Yes | No | Yes |
| `together` | Yes | No | No | Yes |
## Step 1: Implement the `ai.Model` Interface
@@ -34,7 +34,7 @@ func TestAIProviderGuideCapabilityMatrixMatchesRegistry(t *testing.T) {
guide := string(b)
for _, row := range ai.CapabilityRows() {
want := fmt.Sprintf("| `%s` | %s | %s | %s | %s | %s |", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream), yesNo(row.ToolStream))
want := fmt.Sprintf("| `%s` | %s | %s | %s | %s |", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream))
if !strings.Contains(guide, want) {
t.Fatalf("AI provider guide capability matrix is stale; missing row %q", want)
}
@@ -69,12 +69,6 @@ SSH access, or remote service is required.
## Run focused checks while iterating
Use the dedicated inner-loop target when you need the provider-free CLI contract in one focused command:
```sh
make inner-loop
```
Use the smaller checks when you are working on one seam:
```sh
+3 -3
View File
@@ -16,14 +16,14 @@ It's built on a pluggable architecture of Go interfaces: service discovery, clie
## Learn More
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp in the same order as the README: `micro agent demo` for the installed no-secret CLI affordance, `micro examples` for the provider-free examples map, `micro zero-to-hero` for the maintained lifecycle harness, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the runnable examples map, [the 0→hero support reference](https://github.com/micro/go-micro/tree/master/examples/support) for the full no-secret lifecycle example, [No-secret first-agent transcript](guides/no-secret-first-agent.html) to run a mock-model support agent, [Your First Agent](guides/your-first-agent.html) to build and chat with a service-backed agent, [Debugging your agent](guides/debugging-agents.html) to use `micro inspect agent <name>` for runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
Start with [Getting Started](getting-started.html) for install and the first local service. Then follow the first-agent on-ramp: `micro agent demo` for the installed no-secret CLI affordance, [examples wayfinding index](https://github.com/micro/go-micro/blob/master/examples/INDEX.md) for the maintained 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 inspect runs and memory, and the [0→hero reference path](guides/zero-to-hero.html) to walk the full scaffold → run → chat → inspect → deploy dry-run lifecycle covered by CI.
Otherwise continue to read the docs for more information about the framework.
## Contents
- [Getting Started](getting-started.html)
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → `micro inspect agent <name>` → deploy dry-run with CI-backed commands
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → inspect → deploy dry-run with CI-backed commands
- `micro agent demo` - Show the provider-free first-agent demo command and next docs steps
- `micro examples` - Show provider-free first-agent examples in copy/paste order
- [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
@@ -52,7 +52,7 @@ Otherwise continue to read the docs for more information about the framework.
## AI & Agents
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → `micro inspect agent <name>` → deploy dry-run with CI-backed commands
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → inspect → 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 end to end
- [Building AI-Native Services](guides/ai-native-services.html) - End-to-end tutorial for MCP-enabled services
+1 -1
View File
@@ -49,7 +49,7 @@ You now have the service half of the services → agents → workflows lifecycle
6. **[Smallest first-agent example](https://github.com/micro/go-micro/tree/master/examples/first-agent)** - run a mock-model, no-secret agent before adding provider keys.
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)** - turn this service into an agent-callable tool, chat with it, and learn the `micro agent preflight``micro run``micro chat` loop.
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 does something surprising.
9. **[Debugging your agent](guides/debugging-agents.html)** - inspect service registration, tool calls, run history, memory, provider failures, and flow handoffs when the agent does something surprising.
10. **[0→hero Reference](guides/zero-to-hero.html)** - walk the maintained scaffold → run → chat → inspect → deploy dry-run path that proves services, agents, and workflows together.
After that first-agent path, branch out to:
+2 -16
View File
@@ -11,7 +11,7 @@ Go Micro is a framework for building **agents and services** in Go. An agent is
The foundation is in place:
- **Services** — register, discover, RPC, events; every endpoint is automatically an MCP tool.
- **Agents** — a model with memory and tools that manages services, with `plan`, `delegate`, guardrails (`MaxSteps`, `LoopLimit`, `ApproveTool`), tool-execution middleware (`WrapTool`), run metadata, checkpoint/resume, and OpenTelemetry run spans built in.
- **Agents** — a model with memory and tools that manages services, with `plan`, `delegate`, and guardrails (`MaxSteps`, `LoopLimit`, `ApproveTool`) built in, plus tool-execution middleware (`WrapTool`) and run metadata.
- **Flows** — durable, event-driven workflows: ordered steps that checkpoint and resume after a crash.
- **Interop** — the MCP gateway (services as tools) and the A2A gateway (agents as agents, both directions, including A2A streaming, push notifications, and multi-turn continuation), both generated from the registry; x402 for paid tools.
- **Secure by default** — TLS verification on, state scoped per component.
@@ -34,24 +34,10 @@ The priority is that what exists works everywhere, under real conditions.
- **Failure & resilience.** Provider timeouts, rate limits, and cancellation mid-run; deadline/`context` propagation through the agent loop; retry and backoff at the model call.
- **The getting-started contract.** Define and CI-verify the 0→1 and 0→hero flows so they can't silently break.
## Shipped agent depth
- **Durable agent loop.** Opt-in `Checkpoint` support now lets agent `Ask` and
streaming runs persist, list pending work, and resume without replaying completed
tool calls. Human-input pauses resume through explicit input helpers.
- **Agent observability.** `RunInfo` now feeds OpenTelemetry spans and events for
agent runs, model turns, tool calls, retries, delegation lineage, and resume
checkpoints so production runs are traceable.
## Next — agentic depth
- **Streaming.** Broaden provider-backed `ai.Stream` coverage and keep chat plus A2A `message/stream` working end to end for real chat and long-task UX.
- **Resume operations polish.** Keep improving CLI/docs breadcrumbs for finding
pending agent runs and deciding whether to call resume, resume-input, or stream
resume in production.
- **Observability hardening.** Keep span attributes and run inspection coherent
across agents, flows, and gateways as more providers and workflow paths are
exercised.
- **Agent observability.** Wire the new `RunInfo` into OpenTelemetry spans so a run — steps, tool calls, delegation — is traceable. This is also what anyone running it in production will need.
## Later