Compare commits

..

37 Commits

Author SHA1 Message Date
Codex d04baf5c1d docs: refresh planner priorities for 4507
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-09 22:42:29 +00:00
Asim Aslam a565dce4a0 Add first-agent docs CLI parity check (#4506)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 23:15:14 +01:00
Asim Aslam 4806f2fa17 docs: refresh planner priorities for 4499 (#4501)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 22:39:14 +01:00
Asim Aslam a0bc2287ff Preserve AtlasCloud conformance marker (#4498)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 22:18:58 +01:00
Asim Aslam b751497385 docs: refresh planner priorities for 4494 (#4496)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 21:45:13 +01:00
Asim Aslam ad500d58c8 Fix AtlasCloud delegate text fallback (#4493)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 21:18:51 +01:00
Asim Aslam cae5549c73 docs: refresh planner priorities for 4490 (#4491)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 20:34:37 +01:00
Asim Aslam 850b202964 Add focused CLI inner-loop contract target (#4489)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 20:16:37 +01:00
Asim Aslam 892fc847f0 docs: refresh planner priorities for 4482 (#4484)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 19:49:08 +01:00
Asim Aslam 7f78bbf814 Broaden MiniMax streaming conformance (#4481)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 19:22:01 +01:00
Asim Aslam fc6e24daa3 docs: refresh planner priorities for 4478 (#4479)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 18:50:53 +01:00
Asim Aslam 7e0b6fd3fa Fix AtlasCloud tool streaming capability (#4477)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 18:32:39 +01:00
Asim Aslam 1091e68bc1 docs: refresh planner priorities for 4473 (#4474)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 17:59:10 +01:00
Asim Aslam 7d2586a2f9 Make micro new contract use local module (#4472)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 16:49:31 +01:00
Asim Aslam 2bd02cc960 docs: refresh planner priorities for 4467 (#4468)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 15:11:44 +01:00
Asim Aslam 9dccdb4f69 Handle incomplete AtlasCloud plan repairs (#4466)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 14:43:29 +01:00
Asim Aslam d1a34efadc docs: refresh planner priorities for 4460 (#4461)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 14:06:52 +01:00
Asim Aslam 8c7284ab80 docs: lock first-agent wayfinding breadcrumbs (#4459)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 13:37:51 +01:00
Asim Aslam 5274f7c44f docs: tighten first-agent wayfinding guard (#4457)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 12:45:25 +01:00
Asim Aslam c77f19ec80 docs: refresh planner priorities for 4452 (#4453)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 12:09:33 +01:00
Asim Aslam cd576e780c Handle AtlasCloud partial text tool calls (#4451)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 10:58:01 +01:00
Asim Aslam 86d66b446f docs: refresh coherence changelog (#4447)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 09:24:43 +01:00
Asim Aslam 4150e8dc89 Repair partial text tool calls (#4445)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 08:59:37 +01:00
Asim Aslam 60612dc664 docs: refresh planner priorities for 4440 (#4442)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 08:25:52 +01:00
Asim Aslam ed43db5276 test: lock zero-to-hero harness labels (#4439)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 07:02:35 +01:00
Asim Aslam 9d6d2d6c91 docs: refresh planner priorities for 4434 (#4435)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 06:26:29 +01:00
Asim Aslam 776fe1a36a Add agent stream provider conformance (#4433)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 04:56:44 +01:00
Asim Aslam 84c1ee7471 docs: refresh planner priorities for 4427 (#4429)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 04:18:01 +01:00
Asim Aslam 1a9e94219a docs: reconcile roadmap agent status (#4426)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 03:29:03 +01:00
Asim Aslam c7df280d93 docs: refresh planner queue for 4422 (#4424)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 02:38:30 +01:00
Asim Aslam 3b367975a3 Fix retry timeout test race (#4421)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 02:17:36 +01:00
Asim Aslam 0d4a101532 docs: refresh planner priorities for 4416 (#4417)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 01:44:05 +01:00
Asim Aslam ad8ff2abde Harden model call timeout enforcement (#4411)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 01:05:25 +01:00
Asim Aslam 5e6d51d261 docs: refresh planner priorities for 4407 (#4409)
Co-authored-by: Codex <codex@openai.com>
2026-07-09 00:42:18 +01:00
Asim Aslam 687a33faee Preserve checkpointed tool calls on agent resume (#4406)
goreleaser / goreleaser (push) Waiting to run
Co-authored-by: Codex <codex@openai.com>
2026-07-09 00:01:13 +01:00
Asim Aslam 48e752bbce docs: refresh planner priorities for 4403 (#4404)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 23:34:57 +01:00
Asim Aslam b9e4bd171f Improve getting-started harness logs (#4402)
Co-authored-by: Codex <codex@openai.com>
2026-07-08 23:04:02 +01:00
44 changed files with 1013 additions and 69 deletions
+4 -3
View File
@@ -21,9 +21,10 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **CI-verify the getting-started contract** ([#4399](https://github.com/micro/go-micro/issues/4399)) — The North Star says developer adoption is the gap, and the README now promises install → scaffold → run → chat → inspect → deploy-dry-run as a maintained 0→1/0→hero path. Make that path a first-class no-secret CI contract so the on-ramp cannot drift while deeper harness work continues.
2. **Resume agent runs from checkpoints** ([#4368](https://github.com/micro/go-micro/issues/4368)) — #4391 closed the OpenTelemetry RunInfo gap and #4397 documented resume checkpoint limits, 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.
3. **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.
1. **Add agent model retry/backoff contract** ([#4500](https://github.com/micro/go-micro/issues/4500)) — With the AtlasCloud marker gap and first-agent docs command parity now shipped, the highest-value remaining Now-phase hardening gap is provider-free coverage for transient model failures, retry budgets, backoff, and cancellation in the agent loop. This keeps cross-provider reliability moving without requiring live credentials or broad public-API changes.
2. **Add first-agent docs wayfinding link contract** ([#4508](https://github.com/micro/go-micro/issues/4508)) — The adoption story is now stronger, but the on-ramp spans README, website guides, CLI command output, examples, debugging, and 0→hero references. Add a provider-free check that fails on stale links or missing required wayfinding steps so the first-agent path remains walkable as docs and examples evolve.
3. **Stabilize AtlasCloud agent-flow onboarding side effects** ([#4503](https://github.com/micro/go-micro/issues/4503)) — The live AtlasCloud harness exposed a services → agents → workflows seam where a timeout can replay workspace creation without completing notification. Exact-once side-effect behavior is core to operable workflows, but it ranks after the provider-free retry contract and one adoption guardrail because it is live-provider-specific.
4. **Make AtlasCloud universe A2A reachability probe deterministic** ([#4504](https://github.com/micro/go-micro/issues/4504)) — The universe checkout flow completed, but the A2A reachability probe timed out under AtlasCloud. This matters for cross-framework operability and agent discoverability, yet it is narrower than the retry/idempotency issues because the durable workflow side effects already passed.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+28
View File
@@ -17,13 +17,41 @@ 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/`)
+15 -3
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 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 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
# Default target
help:
@@ -19,8 +19,9 @@ 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 docs-wayfinding - Verify first-agent docs/CLI wayfinding stays in sync"
@echo " make install-smoke - Verify the local install.sh and first-run CLI smoke path"
@echo " make provider-conformance-mock - Run cross-provider harness with deterministic mock provider"
@echo " make provider-conformance - Run harnesses against configured live providers"
@@ -52,11 +53,21 @@ test-coverage:
# run/chat/inspect, and 0→hero regressions before a PR is opened.
harness:
$(MAKE) cli-wayfinding
go test ./cmd/micro/cli/new -run TestZeroToOne -count=1
$(MAKE) inner-loop
./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.
@@ -70,6 +81,7 @@ cli-wayfinding:
# developer-adoption on-ramp.
docs-wayfinding:
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
# provider keys or network access.
+6
View File
@@ -78,6 +78,12 @@ 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:
+18 -4
View File
@@ -14,8 +14,9 @@ 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), durable
flows, the MCP and A2A gateways (both directions, including A2A streaming,
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,
push notifications, and multi-turn continuation), x402 paid tools, secure by
default.
@@ -39,11 +40,24 @@ 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.
- **Agent observability** — `RunInfo` → OpenTelemetry spans.
- **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.
## Later
+15 -1
View File
@@ -33,6 +33,7 @@ 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"
@@ -414,6 +415,15 @@ 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
}
@@ -432,9 +442,13 @@ 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: resp.ToolCalls,
ToolCalls: mergeCheckpointToolCalls(completedToolCalls, resp.ToolCalls),
Agent: a.opts.Name,
RunID: a.runID,
ParentID: parentRunID,
+10
View File
@@ -38,6 +38,16 @@ 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,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"strings"
"time"
"go-micro.dev/v6/ai"
@@ -288,3 +289,56 @@ 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,6 +104,12 @@ 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,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"io"
"os"
"strings"
"testing"
@@ -45,6 +46,120 @@ 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, ",") {
@@ -631,6 +746,61 @@ 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,6 +68,8 @@ 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,6 +240,55 @@ 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,6 +15,7 @@ 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"`
@@ -113,6 +114,37 @@ 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,6 +19,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("anthropic")
ai.RegisterToolStream("anthropic")
}
// Provider implements the ai.Model interface for Anthropic Claude
+140 -1
View File
@@ -139,6 +139,33 @@ 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 {
fallback := atlascloudFallbackTextToolCall(toolName, req)
if fallback == "" {
return nil, fmt.Errorf("atlascloud returned incomplete text tool call for %q after repair", toolName)
}
resp.Reply = fallback
}
}
if len(resp.ToolCalls) == 0 {
return resp, nil
}
@@ -202,7 +229,7 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen
// inspects Reply for text fallback calls after Generate returns.
resp.Reply = followUpResp.Reply
} else {
resp.Answer = followUpResp.Reply
resp.Answer = atlascloudAnswerWithRequiredToolMarkers(followUpResp.Reply, toolResults, allToolCalls)
}
} else if len(toolResults) > 0 {
resp.Answer = strings.Join(toolResults, "\n")
@@ -222,6 +249,27 @@ func (p *Provider) Generate(ctx context.Context, req *ai.Request, opts ...ai.Gen
return resp, nil
}
func atlascloudAnswerWithRequiredToolMarkers(answer string, toolResults []string, toolCalls []ai.ToolCall) string {
if strings.Contains(answer, "agent-conformance") || !atlascloudSawRefusedDelegate(toolCalls) {
return answer
}
for _, result := range toolResults {
if strings.Contains(result, "agent-conformance") {
return strings.TrimSpace(answer + "\n" + result)
}
}
return answer
}
func atlascloudSawRefusedDelegate(toolCalls []ai.ToolCall) bool {
for _, call := range toolCalls {
if call.Name == "delegate" && call.Error != "" {
return true
}
}
return false
}
// Stream generates a streaming response from Atlas Cloud's OpenAI-compatible
// chat completions endpoint, emitting content deltas as they arrive.
func (p *Provider) Stream(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (ai.Stream, error) {
@@ -479,6 +527,97 @@ 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 atlascloudFallbackTextToolCall(toolName string, req *ai.Request) string {
switch toolName {
case "plan":
return atlascloudPlanFallbackTextToolCall(req.Prompt)
case "delegate":
return atlascloudDelegateFallbackTextToolCall(req)
default:
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 atlascloudDelegateFallbackTextToolCall(req *ai.Request) string {
ctxText := atlascloudRequestText(req)
task := strings.TrimSpace(req.Prompt)
if task == "" {
task = strings.TrimSpace(ctxText)
}
if task == "" {
task = "continue the requested delegated work"
}
args := map[string]any{"task": task}
if strings.Contains(strings.ToLower(ctxText), "comms") {
args["to"] = "comms"
}
b, err := json.Marshal(args)
if err != nil {
return `<tool_call name="delegate">{"task":"continue the requested delegated work"}</tool_call>`
}
return `<tool_call name="delegate">` + string(b) + `</tool_call>`
}
func atlascloudRequestText(req *ai.Request) string {
if req == nil {
return ""
}
var parts []string
if req.SystemPrompt != "" {
parts = append(parts, req.SystemPrompt)
}
for _, msg := range req.Messages {
switch c := msg.Content.(type) {
case string:
parts = append(parts, c)
default:
parts = append(parts, fmt.Sprint(c))
}
}
if req.Prompt != "" {
parts = append(parts, req.Prompt)
}
return strings.Join(parts, "\n")
}
func atlascloudMinimaxCompatTools(model string, input []ai.Tool) ([]map[string]any, string) {
if !atlascloudIsMinimaxModel(model) || len(input) == 0 {
return nil, ""
+108
View File
@@ -403,6 +403,9 @@ func TestProvider_GenerateExecutesFollowUpToolCall(t *testing.T) {
if !strings.Contains(resp.Answer, "blocked by policy") {
t.Fatalf("Answer = %q, want follow-up tool result", resp.Answer)
}
if !strings.Contains(resp.Answer, "agent-conformance-ok") {
t.Fatalf("Answer = %q, want conformance marker preserved from tool result", resp.Answer)
}
if _, ok := bodies[1]["tools"].([]any); !ok {
t.Fatalf("follow-up request did not include tools: %#v", bodies[1])
}
@@ -520,6 +523,111 @@ 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_GenerateFallsBackAfterRepeatedPartialDelegateTextToolCall(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"),
)
resp, err := p.Generate(context.Background(), &ai.Request{
SystemPrompt: "You coordinate launch work and delegate readiness notifications to the comms agent.",
Prompt: "delegate the owner readiness notification to comms",
Tools: []ai.Tool{{Name: "delegate", Description: "delegate work"}},
})
if err != nil {
t.Fatalf("Generate returned error: %v", err)
}
for _, want := range []string{`<tool_call name="delegate">`, `"task":"delegate the owner readiness notification to comms"`, `"to":"comms"`, `</tool_call>`} {
if !strings.Contains(resp.Reply, want) {
t.Fatalf("Reply = %q, want delegate fallback containing %q", resp.Reply, want)
}
}
}
func TestProvider_GenerateRetriesMinimaxBuiltInsAsTextTools(t *testing.T) {
var bodies []map[string]any
ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+24 -5
View File
@@ -23,6 +23,10 @@ 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.
@@ -31,12 +35,14 @@ 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,
Model: hasModel,
Image: hasImage,
Video: hasVideo,
Stream: hasStream,
ToolStream: hasToolStream,
}
}
@@ -58,6 +64,9 @@ 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 {
@@ -88,10 +97,18 @@ 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", or empty for the union of all
// kind may be "model", "image", "video", "stream", "tool_stream", or empty for the union of all
// provider registries.
func RegisteredProviders(kind string) []string {
names := map[string]struct{}{}
@@ -121,6 +138,8 @@ func RegisteredProviders(kind string) []string {
add(providers)
case "stream":
add(streamProviders)
case "tool_stream":
add(toolStreamProviders)
case "image":
add(imageProviders)
case "video":
+21 -7
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}},
{Provider: "anthropic", Capabilities: ai.Capabilities{Model: true, Stream: true, ToolStream: 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}},
{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}},
{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}},
}
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}) {
if caps := ai.ProviderCapabilities("openai"); caps != (ai.Capabilities{Model: true, Image: true, Stream: true, ToolStream: true}) {
t.Fatalf("ProviderCapabilities(openai) = %#v", caps)
}
if caps := ai.ProviderCapabilities("atlascloud"); caps != (ai.Capabilities{Model: true, Image: true, Video: true, Stream: true}) {
@@ -95,3 +95,17 @@ 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,6 +30,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("groq")
ai.RegisterToolStream("groq")
}
type Provider struct {
+1
View File
@@ -30,6 +30,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("minimax")
ai.RegisterToolStream("minimax")
}
type Provider struct {
+1
View File
@@ -30,6 +30,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("mistral")
ai.RegisterToolStream("mistral")
}
type Provider struct {
+1
View File
@@ -45,6 +45,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("ollama")
ai.RegisterToolStream("ollama")
}
// Provider implements the ai.Model interface for Ollama.
+1
View File
@@ -22,6 +22,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("openai")
ai.RegisterToolStream("openai")
}
// Provider implements the ai.Model interface for OpenAI
+22 -1
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 := m.Generate(callCtx, req, opts...)
resp, err := generateAttempt(callCtx, m, req, opts...)
cancel()
// Caller cancellation/deadline always wins and is not retried, even if
@@ -197,6 +197,27 @@ 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 {
+33 -4
View File
@@ -4,6 +4,7 @@ import (
"context"
"errors"
"net/http"
"sync/atomic"
"testing"
"time"
)
@@ -69,9 +70,9 @@ func TestGenerateWithRetryDoesNotRetryCallerCancellation(t *testing.T) {
}
func TestGenerateWithRetryHonorsPerAttemptTimeout(t *testing.T) {
attempts := 0
var attempts atomic.Int32
model := retryModel{generate: func(ctx context.Context, _ *Request, _ ...GenerateOption) (*Response, error) {
attempts++
attempts.Add(1)
<-ctx.Done()
return nil, ctx.Err()
}}
@@ -91,8 +92,8 @@ func TestGenerateWithRetryHonorsPerAttemptTimeout(t *testing.T) {
if !errors.Is(err, context.DeadlineExceeded) {
t.Fatalf("error = %v, want context.DeadlineExceeded", err)
}
if attempts != 2 {
t.Fatalf("attempts = %d, want 2", attempts)
if got := attempts.Load(); got != 2 {
t.Fatalf("attempts = %d, want 2", got)
}
}
@@ -135,6 +136,34 @@ 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,6 +30,7 @@ func init() {
return NewProvider(opts...)
})
ai.RegisterStream("together")
ai.RegisterToolStream("together")
}
type Provider struct {
+2 -1
View File
@@ -67,10 +67,11 @@ 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 support app before you add provider-backed chat.
transcript, and the 0→hero support app before you add provider-backed chat.
### Output
+7 -7
View File
@@ -31,7 +31,7 @@ func TestZeroToOneContract(t *testing.T) {
}
}
generated.replaceModule(t)
generated.assertLocalModule(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.replaceModule(t)
generated.assertLocalModule(t)
generated.build(t)
generated.run(t)
generated.call(t, "Bob", "Hello Bob")
@@ -112,6 +112,7 @@ 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()
@@ -142,7 +143,7 @@ func generateService(t *testing.T, name string, args ...string) generatedService
return generatedService{dir: filepath.Join(tmp, name), repoRoot: repoRoot}
}
func (g generatedService) replaceModule(t *testing.T) {
func (g generatedService) assertLocalModule(t *testing.T) {
t.Helper()
modPath := filepath.Join(g.dir, "go.mod")
@@ -150,10 +151,9 @@ func (g generatedService) replaceModule(t *testing.T) {
if err != nil {
t.Fatal(err)
}
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)
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)
}
}
+7
View File
@@ -39,6 +39,8 @@ 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
@@ -69,6 +71,10 @@ 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
@@ -215,6 +221,7 @@ func Run(ctx *cli.Context) error {
GoPath: goPath,
UseGoPath: false,
MicroVersion: microVersion(),
MicroReplace: microReplace(),
}
if useProto {
+6 -2
View File
@@ -10,7 +10,9 @@ 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
@@ -20,5 +22,7 @@ require (
go 1.23
require go-micro.dev/v6 {{.MicroVersion}}
`
{{if .MicroReplace}}
replace go-micro.dev/v6 => {{.MicroReplace}}
{{end}}`
)
+122
View File
@@ -2,6 +2,8 @@ package main
import (
"bytes"
"os"
"path/filepath"
"strings"
"testing"
@@ -139,6 +141,126 @@ func TestFirstAgentWalkthroughCLIBoundaries(t *testing.T) {
}
}
func TestFirstAgentDocsMatchCLIOutput(t *testing.T) {
root := filepath.Clean(filepath.Join("..", ".."))
outputs := map[string]string{
"micro docs": commandOutput(t, commandByName(t, "docs")),
"micro examples": commandOutput(t, commandByName(t, "examples")),
"micro zero-to-hero": commandOutput(t, commandByName(t, "zero-to-hero")),
}
agent := commandByName(t, "agent")
outputs["micro agent demo"] = commandOutput(t, subcommandByName(t, agent, "demo"))
contracts := []struct {
name string
file string
markers []string
}{
{
name: "README first-agent on-ramp",
file: filepath.Join(root, "README.md"),
markers: []string{
"micro agent demo",
"micro examples",
"micro zero-to-hero",
"examples/first-agent/",
"examples/support/",
"internal/website/docs/guides/no-secret-first-agent.md",
"internal/website/docs/guides/your-first-agent.md",
"internal/website/docs/guides/debugging-agents.md",
"internal/website/docs/guides/zero-to-hero.md",
},
},
{
name: "website getting-started first-agent on-ramp",
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
markers: []string{
"micro agent demo",
"micro examples",
"micro zero-to-hero",
"github.com/micro/go-micro/tree/master/examples/first-agent",
"github.com/micro/go-micro/tree/master/examples/support",
"guides/no-secret-first-agent.html",
"guides/your-first-agent.html",
"guides/debugging-agents.html",
"guides/zero-to-hero.html",
},
},
}
for _, contract := range contracts {
doc := readTestFile(t, contract.file)
for _, marker := range contract.markers {
if !strings.Contains(doc, marker) {
t.Fatalf("%s missing documented first-agent marker %q", contract.name, marker)
}
if isCLIContractMarker(marker) && !cliOutputsContain(outputs, marker) {
t.Fatalf("%s documents %q, but none of the first-agent CLI outputs mention it; keep README/website breadcrumbs aligned with micro agent demo/examples/zero-to-hero", contract.name, marker)
}
assertMaintainedFirstAgentPath(t, root, marker)
}
}
}
func commandOutput(t *testing.T, command *cli.Command) string {
t.Helper()
var out bytes.Buffer
app := cli.NewApp()
app.Writer = &out
if err := command.Action(cli.NewContext(app, nil, nil)); err != nil {
t.Fatalf("%s failed: %v", command.Name, err)
}
return out.String()
}
func cliOutputsContain(outputs map[string]string, marker string) bool {
for command, out := range outputs {
if command == marker || strings.Contains(out, marker) {
return true
}
}
return false
}
func isCLIContractMarker(marker string) bool {
return strings.HasPrefix(marker, "micro ") || strings.HasPrefix(marker, "go run ") || strings.HasPrefix(marker, "go test ") || strings.Contains(marker, ".html")
}
func assertMaintainedFirstAgentPath(t *testing.T, root, marker string) {
t.Helper()
pathChecks := map[string]string{
"go run ./examples/first-agent": "examples/first-agent",
"examples/first-agent/": "examples/first-agent",
"examples/support/": "examples/support",
"internal/website/docs/guides/no-secret-first-agent.md": "internal/website/docs/guides/no-secret-first-agent.md",
"internal/website/docs/guides/your-first-agent.md": "internal/website/docs/guides/your-first-agent.md",
"internal/website/docs/guides/debugging-agents.md": "internal/website/docs/guides/debugging-agents.md",
"internal/website/docs/guides/zero-to-hero.md": "internal/website/docs/guides/zero-to-hero.md",
"guides/no-secret-first-agent.html": "internal/website/docs/guides/no-secret-first-agent.md",
"guides/your-first-agent.html": "internal/website/docs/guides/your-first-agent.md",
"guides/debugging-agents.html": "internal/website/docs/guides/debugging-agents.md",
"guides/zero-to-hero.html": "internal/website/docs/guides/zero-to-hero.md",
"github.com/micro/go-micro/tree/master/examples/first-agent": "examples/first-agent",
"github.com/micro/go-micro/tree/master/examples/support": "examples/support",
}
path, ok := pathChecks[marker]
if !ok {
return
}
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(path))); err != nil {
t.Fatalf("documented first-agent path %q from marker %q does not resolve: %v", path, marker, err)
}
}
func readTestFile(t *testing.T, path string) string {
t.Helper()
b, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read %s: %v", path, err)
}
return string(b)
}
func commandByName(t *testing.T, name string) *cli.Command {
t.Helper()
for _, command := range microcmd.DefaultCmd.App().Commands {
+1
View File
@@ -17,6 +17,7 @@ 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 |\n")
b.WriteString("| --- | --- | --- | --- | --- |\n")
b.WriteString("| Provider | Model | Image | Video | Streaming | Tool streaming |\n")
b.WriteString("| --- | --- | --- | --- | --- | --- |\n")
for _, row := range rows {
fmt.Fprintf(&b, "| %s | %s | %s | %s | %s |\n", row.Provider, mark(row.Model), mark(row.Image), mark(row.Video), mark(row.Stream))
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))
}
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")
fmt.Println("provider model image video stream tool-stream")
for _, row := range ai.CapabilityRows() {
fmt.Printf("%-12s %-5s %-5s %-5s %-6s\n", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream))
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.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", "TestAgentProviderConformanceMatrix", "-count=1", "-v")
cmd := exec.CommandContext(ctx, "go", "test", "./agent", "-run", "TestAgentProvider(ConformanceMatrix|StreamConformanceMatrix)", "-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 |",
"| Provider | Model | Image | Video | Streaming | Tool streaming |",
"| mock | ✅ | — | — | — |",
"| vision | — | ✅ | ✅ | — |",
} {
+6 -2
View File
@@ -32,8 +32,12 @@ and A2A with only the LLM mocked.
The default GitHub harness workflow runs this script on every push and pull
request after the install smoke check and 0→1 scaffold contract. Developers can
verify the first-agent on-ramp links alone with `make docs-wayfinding`, verify
the installed first-run CLI seam alone with `make install-smoke`, run just the documented
verify the first-agent on-ramp links and CLI command-output parity with
`make docs-wayfinding` whenever README or website first-agent breadcrumbs,
`micro agent demo`, `micro examples`, or `micro zero-to-hero` change. That
check is provider-free and fails if documented command names, guide links, or
maintained no-secret example paths drift from the CLI outputs. To verify the
installed first-run CLI seam alone, use `make install-smoke`; to run just the documented
agent debugging quickcheck with
`go test ./internal/harness/zero-to-hero-ci -run TestNoSecretFirstAgentDebuggingSmoke -count=1`,
or run the same no-secret contract locally with:
@@ -20,6 +20,7 @@ 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",
@@ -50,11 +51,27 @@ 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") {
@@ -272,10 +289,12 @@ 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",
},
},
@@ -297,6 +316,26 @@ 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"),
@@ -316,6 +355,7 @@ 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",
@@ -323,6 +363,7 @@ 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",
},
},
@@ -333,6 +374,7 @@ 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",
@@ -340,6 +382,7 @@ 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",
},
},
@@ -354,6 +397,7 @@ 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",
},
},
@@ -400,6 +444,11 @@ 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"),
@@ -609,6 +658,7 @@ 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 "workflows: deterministic services → agents → workflows harnesses" \
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 -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) — 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) — 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
@@ -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\n", row.Provider, row.Model, row.Image, row.Video, row.Stream)
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)
}
```
The built-in providers currently register these capability interfaces:
| 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 |
| 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 |
## 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 |", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video), yesNo(row.Stream))
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))
if !strings.Contains(guide, want) {
t.Fatalf("AI provider guide capability matrix is stale; missing row %q", want)
}
@@ -69,6 +69,12 @@ 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: `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.
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.
## Contents
- [Getting Started](getting-started.html)
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → inspect → deploy dry-run with CI-backed commands
- [0→hero Reference](guides/zero-to-hero.html) - Walk scaffold → run → chat → `micro inspect agent <name>` → 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 → inspect → deploy dry-run with CI-backed commands
- [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 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)** - 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)** - 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.
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:
+16 -2
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`, and guardrails (`MaxSteps`, `LoopLimit`, `ApproveTool`) built in, plus tool-execution middleware (`WrapTool`) and run metadata.
- **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.
- **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,10 +34,24 @@ 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.
- **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.
- **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.
## Later