Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50783210d0 |
@@ -1,48 +0,0 @@
|
||||
name: Architecture Review
|
||||
|
||||
# Periodic high-altitude review of the whole framework and harness against the
|
||||
# North Star (internal/docs/THESIS.md) — part of the autonomous loop
|
||||
# (internal/docs/CONTINUOUS_IMPROVEMENT.md). Where DevRel watches the public
|
||||
# story, the architect watches the system: API coherence, lifecycle gaps, and
|
||||
# whether recent increments are converging on the thesis or sprawling.
|
||||
#
|
||||
# The architect's OUTPUT is an assessment plus scoped follow-up issues that feed
|
||||
# the hourly increment loop — NOT large refactors. Breaking public-API and
|
||||
# architectural changes stay with the human (see CONTINUOUS_IMPROVEMENT.md).
|
||||
#
|
||||
# Opens a fresh issue and dispatches Codex via CODEX_TRIGGER_TOKEN.
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 8 */3 * *" # roughly every 3 days, 08:00 UTC (tunable)
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: architecture-review
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Open an architecture review issue and dispatch Codex
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CODEX_TRIGGER_TOKEN || github.token }}
|
||||
HAS_TRIGGER_TOKEN: ${{ secrets.CODEX_TRIGGER_TOKEN != '' }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
if [ "$HAS_TRIGGER_TOKEN" != "true" ]; then
|
||||
echo "CODEX_TRIGGER_TOKEN is not set — skipping (Codex ignores Actions-bot comments)."
|
||||
exit 0
|
||||
fi
|
||||
ISSUE_URL=$(gh issue create --repo "$REPO" \
|
||||
--title "Architecture review #$RUN_NUMBER" \
|
||||
--body "Periodic architecture / harness review against the North Star in internal/docs/THESIS.md. Output: an assessment plus scoped follow-up issues for the increment loop.")
|
||||
ISSUE_NUM="${ISSUE_URL##*/}"
|
||||
echo "Opened issue #$ISSUE_NUM — dispatching Codex (Architect)."
|
||||
gh issue comment "$ISSUE_NUM" --repo "$REPO" --body \
|
||||
"@codex Act as the architect for go-micro. Review the overall framework and harness against the North Star in internal/docs/THESIS.md (services → agents → workflows as one runtime) and the roadmap in ROADMAP.md. Assess: API coherence and consistency across the core packages (agent, ai, flow, gateway/mcp, gateway/a2a, model, server, store, registry), gaps or missing pieces in the services → agents → workflows lifecycle, duplication or drift, and whether recent increments (scan recently merged PRs) are converging on the thesis or sprawling. Then: (A) post a concise architectural assessment as a comment on this issue (#$ISSUE_NUM) — strengths, the top risks/gaps, and a recommended direction for the next increments; (B) file concrete, scoped follow-up issues for the highest-value gaps so the hourly increment loop can pick them up — \`gh issue create --label codex --label enhancement --title \"<scoped task>\" --body \"<goal, scope, acceptance criteria>\"\` (each must be a single, self-contained, CI-verifiable chunk). Do NOT make breaking public-API or architectural changes yourself — your output is the assessment and the issues. A small, safe doc/comment correction may be a PR (\`git switch -c codex/architect-$ISSUE_NUM\` … \`gh pr create --base master --label codex …\` … \`gh pr merge --squash --auto --delete-branch\`). Do not use the make_pr tool (it is a no-op stub)."
|
||||
@@ -1,47 +0,0 @@
|
||||
name: DevRel Review
|
||||
|
||||
# Daily higher-altitude coherence pass over the PUBLIC surface — README,
|
||||
# website (landing + docs), and blog — part of the autonomous loop
|
||||
# (internal/docs/CONTINUOUS_IMPROVEMENT.md). The hourly increment loop ships
|
||||
# code; this keeps the story coherent: docs/website aligned, README crisp, and
|
||||
# a steady supply of things worth blogging about.
|
||||
#
|
||||
# Like the increment loop it opens a fresh issue and dispatches Codex via
|
||||
# CODEX_TRIGGER_TOKEN (Codex ignores Actions-bot comments). Autonomy boundary:
|
||||
# SAFE factual-alignment and crispness fixes auto-merge; brand/positioning copy
|
||||
# and blog drafts are surfaced in the report for the human, never auto-merged.
|
||||
|
||||
on:
|
||||
workflow_dispatch: {}
|
||||
schedule:
|
||||
- cron: "0 7 * * *" # daily, 07:00 UTC (tunable)
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
|
||||
concurrency:
|
||||
group: devrel-review
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
dispatch:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Open a DevRel review issue and dispatch Codex
|
||||
env:
|
||||
GH_TOKEN: ${{ secrets.CODEX_TRIGGER_TOKEN || github.token }}
|
||||
HAS_TRIGGER_TOKEN: ${{ secrets.CODEX_TRIGGER_TOKEN != '' }}
|
||||
REPO: ${{ github.repository }}
|
||||
RUN_NUMBER: ${{ github.run_number }}
|
||||
run: |
|
||||
if [ "$HAS_TRIGGER_TOKEN" != "true" ]; then
|
||||
echo "CODEX_TRIGGER_TOKEN is not set — skipping (Codex ignores Actions-bot comments)."
|
||||
exit 0
|
||||
fi
|
||||
ISSUE_URL=$(gh issue create --repo "$REPO" \
|
||||
--title "DevRel coherence review #$RUN_NUMBER" \
|
||||
--body "Daily DevRel / coherence pass over README, website (landing + docs), and the blog. North Star: internal/docs/THESIS.md.")
|
||||
ISSUE_NUM="${ISSUE_URL##*/}"
|
||||
echo "Opened issue #$ISSUE_NUM — dispatching Codex (DevRel)."
|
||||
gh issue comment "$ISSUE_NUM" --repo "$REPO" --body \
|
||||
"@codex Act as DevRel for go-micro. Audit the PUBLIC surface — \`README.md\`, \`internal/website/\` (landing \`index.html\` + \`docs/\`), and the blog under \`internal/website/blog/\` — for coherence with the North Star in internal/docs/THESIS.md (an agent harness and service framework; the services → agents → workflows lifecycle). Look for: (1) places where README / website / docs contradict each other, are stale, or describe behavior that has since changed (cross-check against the code and recent merged PRs / CHANGELOG.md); (2) whether the README is crisp and leads with the harness positioning; (3) one to three genuinely blog-worthy items from recently shipped work. Then do BOTH of these: (A) post a concise findings report as a comment on this issue (#$ISSUE_NUM) — what is aligned, what drifted, what you fixed, and the blog ideas; (B) for SAFE factual-alignment and crispness fixes only (NOT brand/marketing/positioning rewrites), open one PR: \`git switch -c codex/devrel-$ISSUE_NUM\`, \`git push -u origin codex/devrel-$ISSUE_NUM\`, \`gh pr create --base master --label codex --title \"<title>\" --body \"<summary, including 'Closes #$ISSUE_NUM'>\"\`, then \`gh pr merge --squash --auto --delete-branch\`. Leave brand/positioning copy and blog drafts for the human — describe them in the report, do NOT open auto-merging PRs for them. Do not use the make_pr tool (it is a no-op stub). If you touch code, verify go build/test/golangci-lint. Stay out of breaking public-API changes."
|
||||
@@ -31,7 +31,7 @@ jobs:
|
||||
run: go run ./internal/harness/universe
|
||||
- name: Agent-flow harness
|
||||
run: go run ./internal/harness/agent-flow
|
||||
- name: 0→hero plan-delegate workflow harness
|
||||
- name: Plan-delegate harness
|
||||
run: go run ./internal/harness/plan-delegate
|
||||
|
||||
harness-live:
|
||||
|
||||
@@ -47,7 +47,7 @@ test-coverage:
|
||||
harness:
|
||||
go run ./internal/harness/universe
|
||||
go run ./internal/harness/agent-flow
|
||||
go run ./internal/harness/plan-delegate # 0→hero: services + agents + flow + plan/delegate
|
||||
go run ./internal/harness/plan-delegate
|
||||
|
||||
# Run the same harnesses against every configured live provider. Providers
|
||||
# without API keys are skipped; configured providers must pass.
|
||||
|
||||
+1
-3
@@ -136,7 +136,7 @@ func (a *agentImpl) setup() {
|
||||
a.tools = ai.NewTools(a.opts.Registry, ai.ToolClient(a.opts.Client))
|
||||
modelOpts = append(modelOpts, ai.WithToolHandler(a.toolHandler()))
|
||||
a.model = ai.New(a.opts.Provider, modelOpts...)
|
||||
if a.model != nil {
|
||||
if a.opts.TraceProvider != nil && a.model != nil {
|
||||
a.model = a.tracedModel(a.model)
|
||||
}
|
||||
|
||||
@@ -241,8 +241,6 @@ func (a *agentImpl) Chat(ctx context.Context, req *pb.ChatRequest, rsp *pb.ChatR
|
||||
}
|
||||
rsp.Reply = resp.Reply
|
||||
rsp.Agent = resp.Agent
|
||||
rsp.RunId = resp.RunID
|
||||
rsp.ParentId = resp.ParentID
|
||||
for _, tc := range resp.ToolCalls {
|
||||
input, _ := json.Marshal(tc.Input)
|
||||
rsp.ToolCalls = append(rsp.ToolCalls, &pb.ToolCall{
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
pb "go-micro.dev/v6/agent/proto"
|
||||
"go-micro.dev/v6/ai"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
@@ -38,28 +34,6 @@ func TestNew(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
a := newTestAgent(Name("chat-run"))
|
||||
var rsp pb.ChatResponse
|
||||
if err := a.Chat(context.Background(), &pb.ChatRequest{Message: "hello"}, &rsp); err != nil {
|
||||
t.Fatalf("Chat: %v", err)
|
||||
}
|
||||
if rsp.RunId == "" {
|
||||
t.Fatal("Chat response RunId is empty")
|
||||
}
|
||||
if rsp.Agent != "chat-run" {
|
||||
t.Errorf("Agent = %q, want chat-run", rsp.Agent)
|
||||
}
|
||||
if rsp.ParentId != "" {
|
||||
t.Errorf("ParentId = %q, want empty", rsp.ParentId)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildPrompt(t *testing.T) {
|
||||
// Custom prompt
|
||||
a := New(Name("test"), Prompt("custom prompt")).(*agentImpl)
|
||||
|
||||
@@ -108,7 +108,6 @@ func (a *agentImpl) toolHandler() ai.ToolHandler {
|
||||
h = a.loopWrap(h)
|
||||
h = a.stepWrap(h)
|
||||
h = a.planWrap(h)
|
||||
h = contextWrap(h)
|
||||
h = a.traceTool(h)
|
||||
for i := len(a.opts.wrappers) - 1; i >= 0; i-- {
|
||||
h = a.opts.wrappers[i](h)
|
||||
@@ -116,21 +115,6 @@ func (a *agentImpl) toolHandler() ai.ToolHandler {
|
||||
return h
|
||||
}
|
||||
|
||||
// contextWrap stops tool execution promptly when the Ask context has
|
||||
// already been canceled or its deadline has expired. This keeps guardrail
|
||||
// bookkeeping and side-effecting tools from running after the caller has
|
||||
// abandoned the agent run.
|
||||
func contextWrap(next ai.ToolHandler) ai.ToolHandler {
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errResult(call.ID, ctx.Err().Error())
|
||||
default:
|
||||
}
|
||||
return next(ctx, call)
|
||||
}
|
||||
}
|
||||
|
||||
// baseHandler executes a tool call: a developer custom tool, the built-in
|
||||
// delegate, or an RPC to the service. It is the innermost handler.
|
||||
func (a *agentImpl) baseHandler() ai.ToolHandler {
|
||||
|
||||
+2
-3
@@ -250,9 +250,8 @@ func WithTool(name, description string, properties map[string]any, handler ToolF
|
||||
}
|
||||
}
|
||||
|
||||
// TraceProvider enables OpenTelemetry tracing for agent runs. The persisted
|
||||
// run timeline is recorded even when TraceProvider is nil; trace/span IDs are
|
||||
// added only when a provider is configured.
|
||||
// TraceProvider enables OpenTelemetry tracing for agent runs. When nil,
|
||||
// agent tracing and run timeline recording are disabled.
|
||||
func TraceProvider(tp trace.TracerProvider) Option {
|
||||
return func(o *Options) { o.TraceProvider = tp }
|
||||
}
|
||||
|
||||
+12
-59
@@ -62,10 +62,6 @@ type RunListOptions struct {
|
||||
// Status, when set, keeps only runs with the matching status
|
||||
// (for example "running", "done", "error", or "refused").
|
||||
Status string
|
||||
// TraceID, when set, keeps only runs correlated with this trace id.
|
||||
// A prefix is accepted so operators can paste the shortened trace id
|
||||
// printed by `micro runs`.
|
||||
TraceID string
|
||||
// Limit, when positive, returns the most recently updated runs up to
|
||||
// the limit. Limited results are ordered newest first.
|
||||
Limit int
|
||||
@@ -92,23 +88,13 @@ func (a *agentImpl) tracer() trace.Tracer {
|
||||
}
|
||||
|
||||
func (a *agentImpl) startRun(ctx context.Context, message string) (context.Context, func(error)) {
|
||||
info, _ := ai.RunInfoFrom(ctx)
|
||||
start := time.Now()
|
||||
|
||||
if a.opts.TraceProvider == nil {
|
||||
a.recordRunEvent(RunEvent{Time: start, RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "run", Name: message})
|
||||
return ctx, func(err error) {
|
||||
latency := time.Since(start).Milliseconds()
|
||||
if err != nil {
|
||||
a.recordRunEvent(RunEvent{Time: time.Now(), RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "error", LatencyMS: latency, Error: err.Error()})
|
||||
return
|
||||
}
|
||||
a.recordRunEvent(RunEvent{Time: time.Now(), RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "done", LatencyMS: latency})
|
||||
}
|
||||
return ctx, func(error) {}
|
||||
}
|
||||
|
||||
info, _ := ai.RunInfoFrom(ctx)
|
||||
ctx, span := a.tracer().Start(ctx, spanNameRun, trace.WithSpanKind(trace.SpanKindInternal), trace.WithAttributes(
|
||||
attribute.String(AttrRunID, info.RunID), attribute.String(AttrParentRunID, info.ParentID), attribute.String(AttrAgentName, info.Agent)))
|
||||
start := time.Now()
|
||||
a.recordSpanEvent(span, RunEvent{Time: start, RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "run", Name: message})
|
||||
return ctx, func(err error) {
|
||||
latency := time.Since(start).Milliseconds()
|
||||
@@ -132,33 +118,14 @@ type tracedModel struct {
|
||||
|
||||
func (a *agentImpl) tracedModel(m ai.Model) ai.Model { return &tracedModel{Model: m, a: a} }
|
||||
func (m *tracedModel) Generate(ctx context.Context, req *ai.Request, opts ...ai.GenerateOption) (*ai.Response, error) {
|
||||
if m.a.opts.TraceProvider == nil {
|
||||
return m.Model.Generate(ctx, req, opts...)
|
||||
}
|
||||
info, _ := ai.RunInfoFrom(ctx)
|
||||
provider := m.String()
|
||||
model := m.Options().Model
|
||||
ctx, span := m.a.tracer().Start(ctx, spanNameModelCall, trace.WithAttributes(attribute.String(AttrProvider, provider), attribute.String(AttrModel, model)))
|
||||
start := time.Now()
|
||||
|
||||
if m.a.opts.TraceProvider == nil {
|
||||
resp, err := m.Model.Generate(ctx, req, opts...)
|
||||
dur := time.Since(start).Milliseconds()
|
||||
usage := ai.Usage{}
|
||||
if resp != nil {
|
||||
usage = resp.Usage
|
||||
}
|
||||
e := RunEvent{Time: time.Now(), RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "model", Provider: provider, Model: model, LatencyMS: dur, Tokens: usage}
|
||||
if err != nil {
|
||||
e.Error = err.Error()
|
||||
}
|
||||
m.a.recordRunEvent(e)
|
||||
return resp, err
|
||||
}
|
||||
|
||||
ctx, span := m.a.tracer().Start(ctx, spanNameModelCall, trace.WithAttributes(
|
||||
attribute.String(AttrRunID, info.RunID),
|
||||
attribute.String(AttrParentRunID, info.ParentID),
|
||||
attribute.String(AttrAgentName, info.Agent),
|
||||
attribute.String(AttrProvider, provider),
|
||||
attribute.String(AttrModel, model),
|
||||
))
|
||||
resp, err := m.Model.Generate(ctx, req, opts...)
|
||||
dur := time.Since(start).Milliseconds()
|
||||
attrs := []attribute.KeyValue{attribute.Int64(AttrLatencyMS, dur)}
|
||||
@@ -197,24 +164,13 @@ func appendUsage(attrs []attribute.KeyValue, u ai.Usage) []attribute.KeyValue {
|
||||
}
|
||||
|
||||
func (a *agentImpl) traceTool(next ai.ToolHandler) ai.ToolHandler {
|
||||
if a.opts.TraceProvider == nil {
|
||||
return next
|
||||
}
|
||||
return func(ctx context.Context, call ai.ToolCall) ai.ToolResult {
|
||||
info, _ := ai.RunInfoFrom(ctx)
|
||||
ctx, span := a.tracer().Start(ctx, spanNameToolCall, trace.WithAttributes(attribute.String(AttrToolName, call.Name), attribute.Bool(AttrDelegate, call.Name == toolDelegate)))
|
||||
start := time.Now()
|
||||
|
||||
if a.opts.TraceProvider == nil {
|
||||
res := next(ctx, call)
|
||||
dur := time.Since(start).Milliseconds()
|
||||
a.recordRunEvent(RunEvent{Time: time.Now(), RunID: info.RunID, ParentID: info.ParentID, Agent: info.Agent, Kind: "tool", Name: call.Name, LatencyMS: dur, Refused: res.Refused, Error: resultError(res)})
|
||||
return res
|
||||
}
|
||||
|
||||
ctx, span := a.tracer().Start(ctx, spanNameToolCall, trace.WithAttributes(
|
||||
attribute.String(AttrRunID, info.RunID),
|
||||
attribute.String(AttrParentRunID, info.ParentID),
|
||||
attribute.String(AttrAgentName, info.Agent),
|
||||
attribute.String(AttrToolName, call.Name),
|
||||
attribute.Bool(AttrDelegate, call.Name == toolDelegate),
|
||||
))
|
||||
res := next(ctx, call)
|
||||
dur := time.Since(start).Milliseconds()
|
||||
attrs := []attribute.KeyValue{attribute.Int64(AttrLatencyMS, dur)}
|
||||
@@ -257,7 +213,7 @@ func (a *agentImpl) recordSpanEvent(span trace.Span, e RunEvent) {
|
||||
}
|
||||
|
||||
func (a *agentImpl) recordRunEvent(e RunEvent) {
|
||||
if e.RunID == "" {
|
||||
if a.opts.TraceProvider == nil || e.RunID == "" {
|
||||
return
|
||||
}
|
||||
b, _ := json.Marshal(e)
|
||||
@@ -336,9 +292,6 @@ func ListRunSummariesWithOptions(s store.Store, agentName string, opts RunListOp
|
||||
if opts.Status != "" && summary.Status != opts.Status {
|
||||
continue
|
||||
}
|
||||
if opts.TraceID != "" && !strings.HasPrefix(summary.TraceID, opts.TraceID) {
|
||||
continue
|
||||
}
|
||||
summaries = append(summaries, summary)
|
||||
}
|
||||
if opts.Limit > 0 {
|
||||
|
||||
+7
-59
@@ -8,7 +8,6 @@ import (
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/store"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
)
|
||||
@@ -47,33 +46,16 @@ func TestAgentOpenTelemetrySpans(t *testing.T) {
|
||||
}
|
||||
spans := exp.GetSpans().Snapshots()
|
||||
want := map[string]bool{spanNameRun: false, spanNameModelCall: false, spanNameToolCall: false}
|
||||
var runID string
|
||||
for _, s := range spans {
|
||||
if _, ok := want[s.Name()]; ok {
|
||||
want[s.Name()] = true
|
||||
}
|
||||
attrs := spanAttributes(s.Attributes())
|
||||
if s.Name() == spanNameRun {
|
||||
runID = attrs[AttrRunID]
|
||||
}
|
||||
}
|
||||
for name, seen := range want {
|
||||
if !seen {
|
||||
t.Fatalf("span %s not emitted; got %d spans", name, len(spans))
|
||||
}
|
||||
}
|
||||
if runID == "" {
|
||||
t.Fatal("run span missing run id attribute")
|
||||
}
|
||||
for _, s := range spans {
|
||||
if s.Name() != spanNameModelCall && s.Name() != spanNameToolCall {
|
||||
continue
|
||||
}
|
||||
attrs := spanAttributes(s.Attributes())
|
||||
if attrs[AttrRunID] != runID || attrs[AttrAgentName] != "runner" {
|
||||
t.Fatalf("%s missing run correlation attributes: %#v", s.Name(), attrs)
|
||||
}
|
||||
}
|
||||
keys, err := store.Scope(st, "agent", "runner").List(store.ListPrefix("runs/"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -109,15 +91,7 @@ func TestAgentOpenTelemetrySpans(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func spanAttributes(attrs []attribute.KeyValue) map[string]string {
|
||||
out := make(map[string]string, len(attrs))
|
||||
for _, attr := range attrs {
|
||||
out[string(attr.Key)] = attr.Value.AsString()
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func TestAgentRunTimelineRecordsModelAndToolWithoutTraceProvider(t *testing.T) {
|
||||
func TestAgentOpenTelemetryNoopWhenUnconfigured(t *testing.T) {
|
||||
st := store.NewMemoryStore()
|
||||
a := New(Name("runner-noop"), Provider("oteltest"), WithStore(st), WithTool("probe", "probe", nil, func(context.Context, map[string]any) (string, error) { return "ok", nil }))
|
||||
if _, err := a.Ask(context.Background(), "hello"); err != nil {
|
||||
@@ -127,37 +101,11 @@ func TestAgentRunTimelineRecordsModelAndToolWithoutTraceProvider(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(keys) == 0 {
|
||||
t.Fatal("expected run timeline without TraceProvider")
|
||||
if len(keys) != 0 {
|
||||
t.Fatalf("expected no run timeline without TraceProvider, got %v", keys)
|
||||
}
|
||||
summaries, err := ListRunSummaries(st, "runner-noop")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(summaries) != 1 {
|
||||
t.Fatalf("got %d summaries, want 1", len(summaries))
|
||||
}
|
||||
if summaries[0].Status != "done" || summaries[0].LastKind != "done" {
|
||||
t.Fatalf("unexpected summary without TraceProvider: %#v", summaries[0])
|
||||
}
|
||||
if summaries[0].TraceID != "" || summaries[0].SpanID != "" {
|
||||
t.Fatalf("unexpected trace correlation without TraceProvider: %#v", summaries[0])
|
||||
}
|
||||
events, err := LoadRunEvents(st, "runner-noop", summaries[0].RunID)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
seen := map[string]bool{"run": false, "model": false, "tool": false, "done": false}
|
||||
for _, e := range events {
|
||||
seen[e.Kind] = true
|
||||
if e.TraceID != "" || e.SpanID != "" {
|
||||
t.Fatalf("event has trace correlation without TraceProvider: %#v", e)
|
||||
}
|
||||
}
|
||||
for kind, ok := range seen {
|
||||
if !ok {
|
||||
t.Fatalf("missing %s event in timeline: %#v", kind, events)
|
||||
}
|
||||
if _, ok := a.(*agentImpl).model.(*tracedModel); ok {
|
||||
t.Fatal("model should not be wrapped when TraceProvider is nil")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -236,7 +184,7 @@ func TestListRunSummariesWithOptionsFiltersAndLimits(t *testing.T) {
|
||||
events := []RunEvent{
|
||||
{Time: time.Unix(0, 1), RunID: "run-old", Agent: "runner", Kind: "run"},
|
||||
{Time: time.Unix(0, 2), RunID: "run-old", Agent: "runner", Kind: "done"},
|
||||
{Time: time.Unix(0, 3), RunID: "run-new", Agent: "runner", TraceID: "abcdef1234567890", Kind: "run"},
|
||||
{Time: time.Unix(0, 3), RunID: "run-new", Agent: "runner", Kind: "run"},
|
||||
{Time: time.Unix(0, 4), RunID: "run-new", Agent: "runner", Kind: "error", Error: "boom"},
|
||||
}
|
||||
for _, e := range events {
|
||||
@@ -249,7 +197,7 @@ func TestListRunSummariesWithOptionsFiltersAndLimits(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
got, err := ListRunSummariesWithOptions(st, "runner", RunListOptions{Status: "error", TraceID: "abcdef", Limit: 1})
|
||||
got, err := ListRunSummariesWithOptions(st, "runner", RunListOptions{Status: "error", Limit: 1})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
+6
-26
@@ -66,14 +66,10 @@ func (x *ChatRequest) GetMessage() string {
|
||||
}
|
||||
|
||||
type ChatResponse struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"`
|
||||
Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"`
|
||||
ToolCalls []*ToolCall `protobuf:"bytes,3,rep,name=tool_calls,json=toolCalls,proto3" json:"tool_calls,omitempty"`
|
||||
// run_id correlates this chat response with tool calls, traces, and run history.
|
||||
RunId string `protobuf:"bytes,4,opt,name=run_id,json=runId,proto3" json:"run_id,omitempty"`
|
||||
// parent_id is set when this response belongs to a delegated sub-agent run.
|
||||
ParentId string `protobuf:"bytes,5,opt,name=parent_id,json=parentId,proto3" json:"parent_id,omitempty"`
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Reply string `protobuf:"bytes,1,opt,name=reply,proto3" json:"reply,omitempty"`
|
||||
Agent string `protobuf:"bytes,2,opt,name=agent,proto3" json:"agent,omitempty"`
|
||||
ToolCalls []*ToolCall `protobuf:"bytes,3,rep,name=tool_calls,json=toolCalls,proto3" json:"tool_calls,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
@@ -129,20 +125,6 @@ func (x *ChatResponse) GetToolCalls() []*ToolCall {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *ChatResponse) GetRunId() string {
|
||||
if x != nil {
|
||||
return x.RunId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ChatResponse) GetParentId() string {
|
||||
if x != nil {
|
||||
return x.ParentId
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
|
||||
@@ -217,14 +199,12 @@ const file_proto_agent_proto_rawDesc = "" +
|
||||
"\n" +
|
||||
"\x11proto/agent.proto\x12\x05agent\"'\n" +
|
||||
"\vChatRequest\x12\x18\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage\"\x9e\x01\n" +
|
||||
"\amessage\x18\x01 \x01(\tR\amessage\"j\n" +
|
||||
"\fChatResponse\x12\x14\n" +
|
||||
"\x05reply\x18\x01 \x01(\tR\x05reply\x12\x14\n" +
|
||||
"\x05agent\x18\x02 \x01(\tR\x05agent\x12.\n" +
|
||||
"\n" +
|
||||
"tool_calls\x18\x03 \x03(\v2\x0f.agent.ToolCallR\ttoolCalls\x12\x15\n" +
|
||||
"\x06run_id\x18\x04 \x01(\tR\x05runId\x12\x1b\n" +
|
||||
"\tparent_id\x18\x05 \x01(\tR\bparentId\"\\\n" +
|
||||
"tool_calls\x18\x03 \x03(\v2\x0f.agent.ToolCallR\ttoolCalls\"\\\n" +
|
||||
"\bToolCall\x12\x0e\n" +
|
||||
"\x02id\x18\x01 \x01(\tR\x02id\x12\x12\n" +
|
||||
"\x04name\x18\x02 \x01(\tR\x04name\x12\x14\n" +
|
||||
|
||||
@@ -17,11 +17,6 @@ message ChatResponse {
|
||||
string reply = 1;
|
||||
string agent = 2;
|
||||
repeated ToolCall tool_calls = 3;
|
||||
|
||||
// run_id correlates this chat response with tool calls, traces, and run history.
|
||||
string run_id = 4;
|
||||
// parent_id is set when this response belongs to a delegated sub-agent run.
|
||||
string parent_id = 5;
|
||||
}
|
||||
|
||||
message ToolCall {
|
||||
|
||||
@@ -3,7 +3,6 @@ package agent
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@@ -76,29 +75,3 @@ func TestAskRetriesTransientErrorsThenSurfacesStructuredError(t *testing.T) {
|
||||
t.Fatalf("model attempts = %d, want 2", attempts)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCanceledAskContextSkipsToolExecution(t *testing.T) {
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
if opts.ToolHandler == nil {
|
||||
t.Fatal("missing tool handler")
|
||||
}
|
||||
canceled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
res := opts.ToolHandler(canceled, ai.ToolCall{ID: "call-1", Name: toolPlan, Input: map[string]any{
|
||||
"steps": []any{map[string]any{"task": "should not persist", "status": "pending"}},
|
||||
}})
|
||||
if !strings.Contains(res.Content, context.Canceled.Error()) {
|
||||
t.Fatalf("tool result = %q, want cancellation error", res.Content)
|
||||
}
|
||||
return &ai.Response{Reply: "ok"}, nil
|
||||
}
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
a := newTestAgent(Name("cancel-tools"))
|
||||
if _, err := a.Ask(context.Background(), "try a canceled tool"); err != nil {
|
||||
t.Fatalf("Ask: %v", err)
|
||||
}
|
||||
if plan := a.loadPlan(); plan != "" {
|
||||
t.Fatalf("plan persisted after canceled tool context: %q", plan)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -188,16 +188,6 @@ type Response struct {
|
||||
- `ToolCalls`: List of tools the model requested (if any)
|
||||
- `Answer`: The final answer after tools are executed (only set if ToolHandler is provided)
|
||||
|
||||
## Provider capability matrix
|
||||
|
||||
The CLI can print the provider capabilities registered in the current build:
|
||||
|
||||
```bash
|
||||
micro ai providers
|
||||
```
|
||||
|
||||
It reports support from Go Micro's provider registry, so the matrix reflects the model, image, and video interfaces available to this binary rather than external provider marketing claims.
|
||||
|
||||
## Supported Providers
|
||||
|
||||
### Anthropic Claude
|
||||
|
||||
+3
-26
@@ -2,13 +2,6 @@ package ai
|
||||
|
||||
import "sort"
|
||||
|
||||
// CapabilityRow is one deterministic row in a provider capability matrix.
|
||||
type CapabilityRow struct {
|
||||
// Provider is the registered provider name.
|
||||
Provider string
|
||||
Capabilities
|
||||
}
|
||||
|
||||
// Capabilities describes the AI interfaces a provider has registered.
|
||||
// It is intentionally based on package registration rather than external
|
||||
// provider marketing claims, so it reflects what this build can actually use.
|
||||
@@ -34,10 +27,9 @@ func ProviderCapabilities(provider string) Capabilities {
|
||||
}
|
||||
}
|
||||
|
||||
// CapabilityMatrix returns a snapshot of all registered AI providers and the
|
||||
// interfaces they support. The returned map is a copy and can be modified by
|
||||
// callers without mutating the registry. Use CapabilityRows when rendering a
|
||||
// deterministic table or report.
|
||||
// CapabilityMatrix returns a stable snapshot of all registered AI providers and
|
||||
// the interfaces they support. The returned map is a copy and can be modified by
|
||||
// callers without mutating the registry.
|
||||
func CapabilityMatrix() map[string]Capabilities {
|
||||
names := map[string]struct{}{}
|
||||
for name := range providers {
|
||||
@@ -57,21 +49,6 @@ func CapabilityMatrix() map[string]Capabilities {
|
||||
return matrix
|
||||
}
|
||||
|
||||
// CapabilityRows returns a deterministic capability support matrix for every
|
||||
// registered AI provider. It is the ordered form of CapabilityMatrix, intended
|
||||
// for CLIs, docs generators, and conformance reports that need stable output.
|
||||
func CapabilityRows() []CapabilityRow {
|
||||
names := RegisteredProviders("")
|
||||
rows := make([]CapabilityRow, 0, len(names))
|
||||
for _, name := range names {
|
||||
rows = append(rows, CapabilityRow{
|
||||
Provider: name,
|
||||
Capabilities: ProviderCapabilities(name),
|
||||
})
|
||||
}
|
||||
return rows
|
||||
}
|
||||
|
||||
// RegisteredProviders returns the registered provider names in sorted order.
|
||||
// kind may be "model", "image", "video", or empty for the union of all
|
||||
// provider registries.
|
||||
|
||||
@@ -34,22 +34,6 @@ func TestRegisteredProviders(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityRows(t *testing.T) {
|
||||
got := ai.CapabilityRows()
|
||||
want := []ai.CapabilityRow{
|
||||
{Provider: "anthropic", Capabilities: ai.Capabilities{Model: true}},
|
||||
{Provider: "atlascloud", Capabilities: ai.Capabilities{Model: true, Image: true, Video: true}},
|
||||
{Provider: "gemini", Capabilities: ai.Capabilities{Model: true}},
|
||||
{Provider: "groq", Capabilities: ai.Capabilities{Model: true}},
|
||||
{Provider: "mistral", Capabilities: ai.Capabilities{Model: true}},
|
||||
{Provider: "openai", Capabilities: ai.Capabilities{Model: true, Image: true}},
|
||||
{Provider: "together", Capabilities: ai.Capabilities{Model: true}},
|
||||
}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("CapabilityRows() = %#v, want %#v", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityMatrix(t *testing.T) {
|
||||
matrix := ai.CapabilityMatrix()
|
||||
|
||||
|
||||
@@ -1,55 +0,0 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/urfave/cli/v2"
|
||||
goai "go-micro.dev/v6/ai"
|
||||
_ "go-micro.dev/v6/ai/anthropic"
|
||||
_ "go-micro.dev/v6/ai/atlascloud"
|
||||
_ "go-micro.dev/v6/ai/gemini"
|
||||
_ "go-micro.dev/v6/ai/groq"
|
||||
_ "go-micro.dev/v6/ai/mistral"
|
||||
_ "go-micro.dev/v6/ai/openai"
|
||||
_ "go-micro.dev/v6/ai/together"
|
||||
"go-micro.dev/v6/cmd"
|
||||
)
|
||||
|
||||
func init() {
|
||||
cmd.Register(&cli.Command{
|
||||
Name: "ai",
|
||||
Usage: "Inspect AI provider support",
|
||||
Subcommands: []*cli.Command{{
|
||||
Name: "providers",
|
||||
Usage: "Print the registered AI provider capability matrix",
|
||||
Action: providersAction,
|
||||
}},
|
||||
})
|
||||
}
|
||||
|
||||
func providersAction(c *cli.Context) error {
|
||||
writeProviderMatrix(c.App.Writer, goai.CapabilityRows())
|
||||
return nil
|
||||
}
|
||||
|
||||
func writeProviderMatrix(w io.Writer, rows []goai.CapabilityRow) {
|
||||
const check = "✓"
|
||||
fmt.Fprintln(w, "Provider Model Image Video")
|
||||
fmt.Fprintln(w, "-------- ----- ----- -----")
|
||||
for _, row := range rows {
|
||||
fmt.Fprintf(w, "%-11s %-6s %-6s %-6s\n",
|
||||
row.Provider,
|
||||
mark(row.Model, check),
|
||||
mark(row.Image, check),
|
||||
mark(row.Video, check),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func mark(ok bool, value string) string {
|
||||
if ok {
|
||||
return value
|
||||
}
|
||||
return "-"
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
package ai
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
goai "go-micro.dev/v6/ai"
|
||||
)
|
||||
|
||||
func TestWriteProviderMatrix(t *testing.T) {
|
||||
rows := []goai.CapabilityRow{
|
||||
{Provider: "atlascloud", Capabilities: goai.Capabilities{Model: true, Image: true, Video: true}},
|
||||
{Provider: "openai", Capabilities: goai.Capabilities{Model: true, Image: true}},
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
writeProviderMatrix(&out, rows)
|
||||
got := out.String()
|
||||
|
||||
for _, want := range []string{
|
||||
"Provider Model Image Video",
|
||||
"atlascloud ✓ ✓ ✓",
|
||||
"openai ✓ ✓ -",
|
||||
} {
|
||||
if !strings.Contains(got, want) {
|
||||
t.Fatalf("matrix output missing %q:\n%s", want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -135,13 +135,12 @@ func runFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json", Usage: "Print run data as JSON for automation"},
|
||||
&cli.StringFlag{Name: "status", Usage: "Only show runs with this status (running, done, error, refused)"},
|
||||
&cli.StringFlag{Name: "trace", Usage: "Only show runs whose trace id matches this full id or prefix"},
|
||||
&cli.IntFlag{Name: "limit", Usage: "Show the most recently updated N runs"},
|
||||
}
|
||||
}
|
||||
|
||||
func runOptions(c *cli.Context) goagent.RunListOptions {
|
||||
return goagent.RunListOptions{Status: c.String("status"), TraceID: c.String("trace"), Limit: c.Int("limit")}
|
||||
return goagent.RunListOptions{Status: c.String("status"), Limit: c.Int("limit")}
|
||||
}
|
||||
|
||||
func printRunIndex(name string, opts goagent.RunListOptions, asJSON bool) error {
|
||||
|
||||
@@ -75,7 +75,6 @@ Examples:
|
||||
ArgsUsage: "[name]",
|
||||
Flags: []cli.Flag{
|
||||
&cli.BoolFlag{Name: "json", Usage: "Print durable run history as JSON for automation"},
|
||||
&cli.BoolFlag{Name: "pending", Usage: "Only show runs that have not completed"},
|
||||
},
|
||||
Action: flowRuns,
|
||||
},
|
||||
@@ -130,33 +129,13 @@ func flowRuns(c *cli.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if c.Bool("pending") {
|
||||
runs = pendingFlowRuns(runs)
|
||||
}
|
||||
if len(runs) == 0 {
|
||||
if c.Bool("pending") {
|
||||
fmt.Printf(" No pending runs recorded for flow %q.\n", name)
|
||||
return nil
|
||||
}
|
||||
fmt.Printf(" No runs recorded for flow %q.\n", name)
|
||||
return nil
|
||||
}
|
||||
return writeFlowRuns(os.Stdout, runs, c.Bool("json"))
|
||||
}
|
||||
|
||||
func pendingFlowRuns(runs []aiflow.Run) []aiflow.Run {
|
||||
if len(runs) == 0 {
|
||||
return nil
|
||||
}
|
||||
pending := make([]aiflow.Run, 0, len(runs))
|
||||
for _, run := range runs {
|
||||
if run.Status != "done" {
|
||||
pending = append(pending, run)
|
||||
}
|
||||
}
|
||||
return pending
|
||||
}
|
||||
|
||||
func writeFlowRuns(w io.Writer, runs []aiflow.Run, asJSON bool) error {
|
||||
if asJSON {
|
||||
enc := json.NewEncoder(w)
|
||||
|
||||
@@ -55,19 +55,3 @@ func TestWriteFlowRunsJSON(t *testing.T) {
|
||||
t.Fatalf("decoded runs = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPendingFlowRunsFiltersCompletedRuns(t *testing.T) {
|
||||
runs := []aiflow.Run{
|
||||
{ID: "run-1", Status: "done"},
|
||||
{ID: "run-2", Status: "failed"},
|
||||
{ID: "run-3", Status: "running"},
|
||||
}
|
||||
|
||||
got := pendingFlowRuns(runs)
|
||||
if len(got) != 2 {
|
||||
t.Fatalf("pendingFlowRuns returned %d runs, want 2: %+v", len(got), got)
|
||||
}
|
||||
if got[0].ID != "run-2" || got[1].ID != "run-3" {
|
||||
t.Fatalf("pending runs = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"go-micro.dev/v6/cmd"
|
||||
|
||||
_ "go-micro.dev/v6/cmd/micro/a2a"
|
||||
_ "go-micro.dev/v6/cmd/micro/ai"
|
||||
_ "go-micro.dev/v6/cmd/micro/api"
|
||||
_ "go-micro.dev/v6/cmd/micro/chat"
|
||||
_ "go-micro.dev/v6/cmd/micro/cli"
|
||||
|
||||
+1
-12
@@ -1,10 +1,6 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
import "time"
|
||||
|
||||
// Options configures a Flow.
|
||||
type Options struct {
|
||||
@@ -44,8 +40,6 @@ type Options struct {
|
||||
// Checkpoint is the durability backend for stepped runs. Nil with
|
||||
// steps present means a store-backed default; set it to swap backends.
|
||||
Checkpoint Checkpoint
|
||||
// TraceProvider emits OpenTelemetry spans for stepped flow runs.
|
||||
TraceProvider trace.TracerProvider
|
||||
// DeleteOnSuccess removes a run's checkpoint when it completes
|
||||
// successfully. Failed runs are always retained. Default: retain all.
|
||||
DeleteOnSuccess bool
|
||||
@@ -138,8 +132,3 @@ func WithCheckpoint(c Checkpoint) Option {
|
||||
func DeleteOnSuccess() Option {
|
||||
return func(o *Options) { o.DeleteOnSuccess = true }
|
||||
}
|
||||
|
||||
// TraceProvider enables OpenTelemetry spans for stepped flow runs and steps.
|
||||
func TraceProvider(tp trace.TracerProvider) Option {
|
||||
return func(o *Options) { o.TraceProvider = tp }
|
||||
}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/codes"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
const flowInstrumentationName = "go-micro.dev/v6/flow"
|
||||
|
||||
const (
|
||||
spanNameFlowRun = "flow.run"
|
||||
spanNameFlowStep = "flow.step"
|
||||
|
||||
AttrFlowRunID = "flow.run.id"
|
||||
AttrFlowName = "flow.name"
|
||||
AttrFlowStepName = "flow.step.name"
|
||||
AttrFlowStatus = "flow.status"
|
||||
AttrFlowAttempts = "flow.step.attempts"
|
||||
AttrFlowLatencyMS = "flow.latency_ms"
|
||||
)
|
||||
|
||||
func (f *Flow) tracer() trace.Tracer {
|
||||
return f.opts.TraceProvider.Tracer(flowInstrumentationName)
|
||||
}
|
||||
|
||||
func (f *Flow) startRunSpan(ctx context.Context, run Run) (context.Context, func(Run, error)) {
|
||||
if f.opts.TraceProvider == nil {
|
||||
return ctx, func(Run, error) {}
|
||||
}
|
||||
ctx, span := f.tracer().Start(ctx, spanNameFlowRun, trace.WithSpanKind(trace.SpanKindInternal), trace.WithAttributes(
|
||||
attribute.String(AttrFlowRunID, run.ID),
|
||||
attribute.String(AttrFlowName, f.name),
|
||||
attribute.String(AttrFlowStatus, run.Status),
|
||||
))
|
||||
start := time.Now()
|
||||
return ctx, func(done Run, err error) {
|
||||
span.SetAttributes(
|
||||
attribute.String(AttrFlowStatus, done.Status),
|
||||
attribute.Int64(AttrFlowLatencyMS, time.Since(start).Milliseconds()),
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
span.End()
|
||||
}
|
||||
}
|
||||
|
||||
func (f *Flow) runStepSpan(ctx context.Context, step Step, in State) (State, int, error) {
|
||||
if f.opts.TraceProvider == nil {
|
||||
return f.runStep(ctx, step, in)
|
||||
}
|
||||
info, _ := ai.RunInfoFrom(ctx)
|
||||
ctx, span := f.tracer().Start(ctx, spanNameFlowStep, trace.WithAttributes(
|
||||
attribute.String(AttrFlowRunID, info.RunID),
|
||||
attribute.String(AttrFlowName, f.name),
|
||||
attribute.String(AttrFlowStepName, step.Name),
|
||||
))
|
||||
start := time.Now()
|
||||
out, attempts, err := f.runStep(ctx, step, in)
|
||||
span.SetAttributes(
|
||||
attribute.Int(AttrFlowAttempts, attempts),
|
||||
attribute.Int64(AttrFlowLatencyMS, time.Since(start).Milliseconds()),
|
||||
)
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
} else {
|
||||
span.SetStatus(codes.Ok, "")
|
||||
}
|
||||
span.End()
|
||||
return out, attempts, err
|
||||
}
|
||||
@@ -1,70 +0,0 @@
|
||||
package flow
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/store"
|
||||
"go.opentelemetry.io/otel/attribute"
|
||||
"go.opentelemetry.io/otel/sdk/trace"
|
||||
"go.opentelemetry.io/otel/sdk/trace/tracetest"
|
||||
)
|
||||
|
||||
func TestFlowOpenTelemetrySpans(t *testing.T) {
|
||||
exp := tracetest.NewInMemoryExporter()
|
||||
tp := trace.NewTracerProvider(trace.WithSyncer(exp))
|
||||
|
||||
step := Step{Name: "inspect", Run: func(ctx context.Context, in State) (State, error) {
|
||||
in.Data = []byte("done")
|
||||
return in, nil
|
||||
}}
|
||||
f := New("observed", WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "observed")), TraceProvider(tp), Steps(step))
|
||||
if err := f.Execute(context.Background(), "start"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
spans := exp.GetSpans().Snapshots()
|
||||
seen := map[string]bool{spanNameFlowRun: false, spanNameFlowStep: false}
|
||||
var runID string
|
||||
for _, span := range spans {
|
||||
attrs := flowSpanAttributes(span.Attributes())
|
||||
switch span.Name() {
|
||||
case spanNameFlowRun:
|
||||
seen[spanNameFlowRun] = true
|
||||
runID = attrs[AttrFlowRunID]
|
||||
if attrs[AttrFlowName] != "observed" || attrs[AttrFlowStatus] != "done" {
|
||||
t.Fatalf("run span attributes = %#v", attrs)
|
||||
}
|
||||
case spanNameFlowStep:
|
||||
seen[spanNameFlowStep] = true
|
||||
if attrs[AttrFlowName] != "observed" || attrs[AttrFlowStepName] != "inspect" {
|
||||
t.Fatalf("step span attributes = %#v", attrs)
|
||||
}
|
||||
}
|
||||
}
|
||||
for name, ok := range seen {
|
||||
if !ok {
|
||||
t.Fatalf("span %s not emitted; got %d spans", name, len(spans))
|
||||
}
|
||||
}
|
||||
if runID == "" {
|
||||
t.Fatal("run span missing run id")
|
||||
}
|
||||
for _, span := range spans {
|
||||
if span.Name() != spanNameFlowStep {
|
||||
continue
|
||||
}
|
||||
attrs := flowSpanAttributes(span.Attributes())
|
||||
if attrs[AttrFlowRunID] != runID {
|
||||
t.Fatalf("step span run id = %q, want %q", attrs[AttrFlowRunID], runID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func flowSpanAttributes(attrs []attribute.KeyValue) map[string]string {
|
||||
out := make(map[string]string, len(attrs))
|
||||
for _, attr := range attrs {
|
||||
out[string(attr.Key)] = attr.Value.AsString()
|
||||
}
|
||||
return out
|
||||
}
|
||||
+1
-6
@@ -383,10 +383,6 @@ func (f *Flow) Pending(ctx context.Context) ([]Run, error) {
|
||||
func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
|
||||
steps := f.opts.Steps
|
||||
ctx = withDeps(ctx, &runDeps{client: f.client, model: f.model, tools: f.toolSet})
|
||||
ctx = ai.WithRunInfo(ctx, ai.RunInfo{RunID: run.ID, Agent: f.name})
|
||||
ctx, finishSpan := f.startRunSpan(ctx, run)
|
||||
var spanErr error
|
||||
defer func() { finishSpan(run, spanErr) }()
|
||||
|
||||
start := stepIndex(steps, run.State.Stage)
|
||||
if start < 0 {
|
||||
@@ -403,10 +399,9 @@ func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
|
||||
run.Steps[i].Status = "in_progress"
|
||||
f.save(ctx, run)
|
||||
|
||||
out, attempts, err := f.runStepSpan(ctx, step, run.State)
|
||||
out, attempts, err := f.runStep(ctx, step, run.State)
|
||||
run.Steps[i].Attempts = attempts
|
||||
if err != nil {
|
||||
spanErr = err
|
||||
run.Steps[i].Status = "failed"
|
||||
run.Steps[i].Error = err.Error()
|
||||
run.Status = "failed"
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
@@ -87,33 +86,6 @@ func TestFlowCheckpointResume(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepContextIncludesRunInfo(t *testing.T) {
|
||||
var got ai.RunInfo
|
||||
step := Step{Name: "inspect", Run: func(ctx context.Context, in State) (State, error) {
|
||||
var ok bool
|
||||
got, ok = ai.RunInfoFrom(ctx)
|
||||
if !ok {
|
||||
t.Fatal("RunInfo missing from step context")
|
||||
}
|
||||
in.Data = []byte("ok")
|
||||
return in, nil
|
||||
}}
|
||||
|
||||
f := New("correlated",
|
||||
WithCheckpoint(StoreCheckpoint(store.NewMemoryStore(), "correlated")),
|
||||
Steps(step),
|
||||
)
|
||||
if err := f.Execute(context.Background(), "start"); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
if got.Agent != "correlated" {
|
||||
t.Fatalf("RunInfo.Agent = %q, want correlated", got.Agent)
|
||||
}
|
||||
if got.RunID == "" {
|
||||
t.Fatal("RunInfo.RunID is empty")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowResumePendingResumesOldestRunsUntilFailure(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
ctx := context.Background()
|
||||
|
||||
@@ -121,29 +121,6 @@ merged PRs. Each scheduled run:
|
||||
actually appears). Re-doing it creates duplicate PRs and stale branches that
|
||||
then block the next run. Wait for the PR, or let it ride.
|
||||
|
||||
## Overseer passes (DevRel + Architect)
|
||||
|
||||
The hourly loop ships increments; two periodic passes keep the *whole* heading in
|
||||
the right direction. Both use the same mechanism (fresh issue → `@codex` →
|
||||
output) but produce direction and coherence, not just code.
|
||||
|
||||
- **DevRel — daily** (`.github/workflows/devrel-review.yml`). Audits the public
|
||||
surface (README, website landing + docs, blog) for coherence with the North
|
||||
Star, README crispness, and blog-worthy material. **Autonomy boundary:** safe
|
||||
factual-alignment and crispness fixes auto-merge like any increment;
|
||||
brand/positioning copy and blog drafts are *surfaced in a report* for the
|
||||
human, never auto-merged.
|
||||
- **Architect — every few days** (`.github/workflows/architecture-review.yml`).
|
||||
Reviews the framework/harness against the thesis: API coherence, lifecycle
|
||||
gaps, drift/sprawl. **Its output is an assessment plus scoped follow-up
|
||||
issues** that feed the hourly increment loop — it does **not** make breaking or
|
||||
architectural changes itself (those stay with the human).
|
||||
|
||||
Together they close the loop: the architect decides *what* should change and files
|
||||
issues, the increment loop *builds* them, and DevRel keeps the public story
|
||||
honest. Cadence is tunable in each workflow's `cron`. Codex is serial, so these
|
||||
passes queue behind any in-flight increment rather than running concurrently.
|
||||
|
||||
## Stop / redirect
|
||||
|
||||
- In-session: `CronDelete <id>` (or end the session).
|
||||
|
||||
@@ -23,15 +23,8 @@ import (
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/flow"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/selector"
|
||||
"go-micro.dev/v6/service"
|
||||
"go-micro.dev/v6/store"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -243,95 +236,52 @@ func main() {
|
||||
fmt.Printf("\n\033[1mPlan & Delegate — live integration harness (provider: %s)\033[0m\n", *provider)
|
||||
fmt.Print("Real services, registry, RPC, agent loop, store, delegation.\n\n")
|
||||
|
||||
reg := registry.NewMemoryRegistry()
|
||||
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))))
|
||||
mem := store.NewMemoryStore()
|
||||
|
||||
// Real services.
|
||||
taskSvc := new(TaskService)
|
||||
task := service.New(service.Name("task"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl))
|
||||
if err := task.Handle(taskSvc); err != nil {
|
||||
fmt.Println("task handle:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
task := micro.NewService("task")
|
||||
task.Handle(new(TaskService))
|
||||
go task.Run()
|
||||
|
||||
notifySvc := new(NotifyService)
|
||||
notify := service.New(service.Name("notify"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl))
|
||||
if err := notify.Handle(notifySvc); err != nil {
|
||||
fmt.Println("notify handle:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
notify := micro.NewService("notify")
|
||||
notify.Handle(new(NotifyService))
|
||||
go notify.Run()
|
||||
|
||||
// Real comms agent (owns notify), registered so delegate reaches it over RPC.
|
||||
comms := agent.New(
|
||||
agent.Name("comms"),
|
||||
agent.Address("127.0.0.1:0"),
|
||||
agent.Services("notify"),
|
||||
agent.Prompt("You handle outbound notifications. Use the notify service."),
|
||||
agent.Provider(*provider), agent.APIKey(apiKey),
|
||||
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
|
||||
comms := micro.NewAgent("comms",
|
||||
micro.AgentServices("notify"),
|
||||
micro.AgentPrompt("You handle outbound notifications. Use the notify service."),
|
||||
micro.AgentProvider(*provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
)
|
||||
go comms.Run()
|
||||
defer comms.Stop()
|
||||
|
||||
// Real conductor agent (owns task), registered so the flow can reach it over RPC.
|
||||
conductor := agent.New(
|
||||
agent.Name("conductor"),
|
||||
agent.Address("127.0.0.1:0"),
|
||||
agent.Services("task"),
|
||||
agent.Prompt("You coordinate launch work. Plan first, create tasks, and delegate notifications to the \"comms\" agent."),
|
||||
agent.Provider(*provider), agent.APIKey(apiKey),
|
||||
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
|
||||
// Real conductor agent (owns task).
|
||||
conductor := micro.NewAgent("conductor",
|
||||
micro.AgentServices("task"),
|
||||
micro.AgentPrompt("You coordinate launch work. Plan first, create tasks, and delegate notifications to the \"comms\" agent."),
|
||||
micro.AgentProvider(*provider),
|
||||
micro.AgentAPIKey(apiKey),
|
||||
)
|
||||
go conductor.Run()
|
||||
defer conductor.Stop()
|
||||
|
||||
fmt.Println("waiting for services + agents to register...")
|
||||
waitForService := func(name string) {
|
||||
deadline := time.Now().Add(5 * time.Second)
|
||||
for time.Now().Before(deadline) {
|
||||
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
|
||||
return
|
||||
}
|
||||
time.Sleep(20 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
for _, name := range []string{"task", "notify", "comms", "conductor"} {
|
||||
waitForService(name)
|
||||
}
|
||||
fmt.Println("waiting for services + comms agent to register...")
|
||||
time.Sleep(3 * time.Second)
|
||||
|
||||
f := flow.New("zero-to-hero",
|
||||
flow.Agent("conductor"),
|
||||
flow.Prompt("Create three launch tasks (Design, Build, Ship), then make sure owner@acme.com is notified: {{.Data}}"),
|
||||
)
|
||||
if err := f.Register(reg, broker.DefaultBroker, cl); err != nil {
|
||||
fmt.Println("flow register:", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Print("\n\033[1m> prompt:\033[0m Create three launch tasks (Design, Build, Ship), then make sure owner@acme.com is notified.\n\n")
|
||||
|
||||
fmt.Print("\n\033[1m> flow:\033[0m services + agents + workflow + plan/delegate, no API key.\n\n")
|
||||
if err := f.Execute(context.Background(), "launch readiness"); err != nil {
|
||||
resp, err := conductor.Ask(context.Background(),
|
||||
"Create three launch tasks: Design, Build, and Ship. Then make sure owner@acme.com is notified that the launch plan is ready.")
|
||||
if err != nil {
|
||||
fmt.Println("\033[31merror:\033[0m", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
if rs := f.Results(); len(rs) > 0 {
|
||||
fmt.Println("\n\033[1m< conductor reply:\033[0m", rs[len(rs)-1].Reply)
|
||||
}
|
||||
fmt.Println("\n\033[1m< conductor reply:\033[0m", resp.Reply)
|
||||
|
||||
// Prove plan was persisted to the real store.
|
||||
if recs, _ := store.Scope(mem, "agent", "conductor").Read("plan"); len(recs) > 0 {
|
||||
if recs, _ := conductor.Options().Store.Read("agent/conductor/plan"); len(recs) > 0 {
|
||||
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
|
||||
} else {
|
||||
fmt.Println("\n\033[31m! plan was not persisted\033[0m")
|
||||
os.Exit(1)
|
||||
}
|
||||
if taskSvc.count() != 3 || notifySvc.count() != 1 {
|
||||
fmt.Printf("\n\033[31m! unexpected side effects: tasks=%d notify=%d\033[0m\n", taskSvc.count(), notifySvc.count())
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("\n\033[32m✓ 0→hero flow complete (services → agents → workflow)\033[0m")
|
||||
fmt.Println("\n\033[32m✓ end-to-end flow complete\033[0m")
|
||||
}
|
||||
|
||||
@@ -22,15 +22,6 @@ import (
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
_ "go-micro.dev/v6/ai/anthropic"
|
||||
_ "go-micro.dev/v6/ai/atlascloud"
|
||||
_ "go-micro.dev/v6/ai/gemini"
|
||||
_ "go-micro.dev/v6/ai/groq"
|
||||
_ "go-micro.dev/v6/ai/mistral"
|
||||
_ "go-micro.dev/v6/ai/openai"
|
||||
_ "go-micro.dev/v6/ai/together"
|
||||
)
|
||||
|
||||
var providerEnv = map[string]string{
|
||||
@@ -48,7 +39,6 @@ func main() {
|
||||
harnessesFlag := flag.String("harnesses", "universe,agent-flow,plan-delegate", "comma-separated harness names under internal/harness")
|
||||
timeoutFlag := flag.Duration("timeout", 10*time.Minute, "timeout per provider/harness run")
|
||||
requireConfiguredFlag := flag.Bool("require-configured", false, "fail when a selected live provider is missing an API key")
|
||||
capabilitiesFlag := flag.Bool("capabilities", true, "print the registered provider capability matrix before running conformance")
|
||||
flag.Parse()
|
||||
|
||||
providers := splitCSV(*providersFlag)
|
||||
@@ -58,10 +48,6 @@ func main() {
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
if *capabilitiesFlag {
|
||||
printCapabilityMatrix()
|
||||
}
|
||||
|
||||
var ran, skipped, failed int
|
||||
for _, provider := range providers {
|
||||
if provider != "mock" && providerKey(provider) == "" {
|
||||
@@ -93,22 +79,6 @@ func main() {
|
||||
}
|
||||
}
|
||||
|
||||
func printCapabilityMatrix() {
|
||||
fmt.Println("Provider capability matrix:")
|
||||
fmt.Println("provider model image video")
|
||||
for _, row := range ai.CapabilityRows() {
|
||||
fmt.Printf("%-12s %-5s %-5s %-5s\n", row.Provider, yesNo(row.Model), yesNo(row.Image), yesNo(row.Video))
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
func yesNo(ok bool) string {
|
||||
if ok {
|
||||
return "yes"
|
||||
}
|
||||
return "no"
|
||||
}
|
||||
|
||||
func validateSelection(providers, harnesses []string) error {
|
||||
if len(providers) == 0 {
|
||||
return fmt.Errorf("no providers selected")
|
||||
|
||||
@@ -3,8 +3,6 @@ package main
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"go-micro.dev/v6/ai"
|
||||
)
|
||||
|
||||
func TestValidateSelectionAcceptsKnownProviderAndHarness(t *testing.T) {
|
||||
@@ -32,23 +30,3 @@ func TestValidateSelectionRejectsUnsafeHarnessName(t *testing.T) {
|
||||
t.Fatalf("validateSelection error = %q, want invalid harness message", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCapabilityMatrixHasRegisteredProviders(t *testing.T) {
|
||||
rows := ai.CapabilityRows()
|
||||
if len(rows) == 0 {
|
||||
t.Fatal("CapabilityRows returned no providers")
|
||||
}
|
||||
|
||||
var foundOpenAI bool
|
||||
for _, row := range rows {
|
||||
if row.Provider == "openai" {
|
||||
foundOpenAI = true
|
||||
if !row.Model || !row.Image || row.Video {
|
||||
t.Fatalf("openai capabilities = %#v, want model+image only", row.Capabilities)
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundOpenAI {
|
||||
t.Fatalf("CapabilityRows = %#v, want openai row", rows)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,8 +50,6 @@ guides:
|
||||
url: /docs/guides/atlascloud-integration.html
|
||||
- title: AI Provider Guide
|
||||
url: /docs/guides/ai-provider-guide.html
|
||||
- title: Provider Conformance
|
||||
url: /docs/guides/provider-conformance.html
|
||||
- title: Comparison
|
||||
url: /docs/guides/comparison.html
|
||||
- title: Migration Guides
|
||||
@@ -82,7 +80,6 @@ search_order:
|
||||
- /docs/plugins.html
|
||||
- /docs/examples/
|
||||
- /docs/examples/realworld/
|
||||
- /docs/guides/provider-conformance.html
|
||||
- /docs/guides/comparison.html
|
||||
- /docs/guides/migration/
|
||||
- /docs/architecture/
|
||||
|
||||
@@ -1,75 +0,0 @@
|
||||
---
|
||||
layout: blog
|
||||
title: "How Go Micro Builds Itself"
|
||||
permalink: /blog/31
|
||||
description: "Go Micro is increasingly built by an autonomous loop of two AI agents — Codex implementing scoped increments, Claude Code orchestrating, the human setting direction. Here's how the loop actually works, including the parts that broke."
|
||||
---
|
||||
|
||||
# How Go Micro Builds Itself
|
||||
|
||||
*June 25, 2026 • By the Go Micro Team*
|
||||
|
||||
Go Micro is an agent harness. The most honest test of that claim is to use agents to build it — so increasingly, we do. A scheduled loop of two AI agents now opens issues, writes increments, and merges its own pull requests against this repo, on a cadence, with a human setting direction rather than typing the code.
|
||||
|
||||
This isn't a stunt. If a harness is good enough to operate a loop that builds itself, that's evidence it's good enough to operate the loop that builds *your* software. So we pointed the thesis at itself and wired up the loop. This post is how it actually works — including the parts that didn't, because the failure modes are the interesting part.
|
||||
|
||||
## Two agents and a human
|
||||
|
||||
The work splits across three roles:
|
||||
|
||||
- **Codex** is the serial builder. It takes one scoped task at a time, implements it, runs the build, tests, and linter, and opens a pull request.
|
||||
- **Claude Code** is the orchestrator: it sets up the machinery, reviews, integrates, and handles the judgment calls Codex shouldn't make alone.
|
||||
- **The human** sets direction and owns taste — brand and positioning copy, breaking public API changes, architectural decisions. Those never merge autonomously.
|
||||
|
||||
Everything else is automated.
|
||||
|
||||
## The mechanism
|
||||
|
||||
Each cycle is deliberately boring, which is the point:
|
||||
|
||||
1. A scheduled workflow opens a **fresh tracking issue** and dispatches Codex on it with a single instruction: pick the highest-value improvement that advances the [North Star](/docs/guides/agent-harness.html), implement it, verify it builds and passes tests and lint, and open a PR.
|
||||
2. Codex does the work on its own branch and opens the PR.
|
||||
3. GitHub **native auto-merge** lands it the moment the required CI checks go green — build, tests, golangci-lint. There is no human approval step. **CI is the only gate**, and that's not an approval, it's just a refusal to ship broken code.
|
||||
|
||||
Every increment is small, single-concern, and reversible. Nothing clever survives that can't pass the same checks a human contributor's PR would.
|
||||
|
||||
## Three altitudes
|
||||
|
||||
One loop only produces increments; it doesn't know whether they're adding up to anything. So there are three passes at different altitudes:
|
||||
|
||||
- An **architect** pass reviews the whole framework against the thesis every few days — API coherence, gaps in the services → agents → workflows lifecycle, drift — and files scoped issues. It decides *what* to build.
|
||||
- The **hourly increment loop** builds those issues.
|
||||
- A **DevRel** pass audits the README, website, docs, and blog each day for coherence, and surfaces things worth writing about. (This post is the kind of thing it's meant to catch.)
|
||||
|
||||
The architect points, the loop builds, DevRel keeps the story honest. Direction flows down; code flows up.
|
||||
|
||||
## The parts that broke
|
||||
|
||||
Wiring an autonomous loop is mostly plumbing and failure modes, which is exactly why it's a good test of a harness. A few we hit:
|
||||
|
||||
- The agent's "open a pull request" tool turned out to be a **stub** — it recorded the PR's title and body and returned them for a downstream step, but never pushed a branch or called the API. The agent cheerfully reported "opened a PR" every time, and no PR ever appeared. The fix was to stop trusting the tool and have the agent push and open the PR itself.
|
||||
- Dispatching every run from a single tracking issue made the agent derive the **same branch name** each time, so the first increment opened a PR and the rest silently collided. One fresh issue per run fixed it.
|
||||
- At one point an increment helpfully rewrote the repo's own agent instructions to point *back* at the broken tool. An autonomous loop will faithfully encode its own mistakes, so the guardrails have to be explicit.
|
||||
|
||||
None of these are exotic. They're the ordinary reality of operating an agent loop: tools that lie, state that collides, instructions that drift. The things the harness ships — observability, durable runs, resilience, guardrails — are the things you reach for the moment you try to run a loop like this in earnest.
|
||||
|
||||
## What it produces
|
||||
|
||||
The increments are unglamorous and real. Recent ones hardened the agent run loop with **OpenTelemetry run timelines** and a `micro runs` command to inspect them, correlated those timelines with trace spans, added **retry backoff** to durable flows, and made flow steps **cancellation-safe** so a canceled run stops retrying instead of burning its budget. Each one landed as a small PR that passed CI on its own.
|
||||
|
||||
That's the texture of the work: not a model writing a framework in one shot, but a loop making it a little better, continuously, under a gate that keeps it honest.
|
||||
|
||||
## The loop is the proof
|
||||
|
||||
We think the future of agentic software is scheduled, looping, work-performing agents — not chat. Go Micro is built by exactly that, against its own repo. The human still sets direction and owns the calls that need taste; CI is the gate; everything is reversible. Within those bounds, the harness builds itself.
|
||||
|
||||
If it can do that, it can build yours.
|
||||
|
||||
---
|
||||
|
||||
*Go Micro is an open source agent harness and service framework for Go. [Star us on GitHub](https://github.com/micro/go-micro).*
|
||||
|
||||
<div class="post-nav">
|
||||
<div><a href="/blog/30">← Go Micro is an Agent Harness</a></div>
|
||||
<div><a href="/blog/">All Posts</a></div>
|
||||
</div>
|
||||
@@ -11,13 +11,6 @@ permalink: /blog/
|
||||
|
||||
<div class="posts">
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/31">How Go Micro Builds Itself</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">June 25, 2026</p>
|
||||
<p>Go Micro is increasingly built by an autonomous loop of two AI agents — Codex writing scoped increments, Claude Code orchestrating, the human setting direction. Here's how the loop actually works, including the parts that broke.</p>
|
||||
<a href="/blog/31">Read more →</a>
|
||||
</article>
|
||||
|
||||
<article style="margin-bottom: 2rem; padding-bottom: 1.5rem; border-bottom: 1px solid #e5e5e5;">
|
||||
<h2 style="margin: 0 0 0.5rem;"><a href="/blog/30">Go Micro is an Agent Harness</a></h2>
|
||||
<p class="meta" style="color: #666; font-size: 0.85rem;">June 24, 2026</p>
|
||||
|
||||
@@ -66,7 +66,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("example",
|
||||
service := micro.NewService(
|
||||
micro.Name("example"),
|
||||
)
|
||||
service.Init()
|
||||
if err := service.Run(); err != nil {
|
||||
|
||||
@@ -31,11 +31,12 @@ Use **mDNS as the default registry** for service discovery.
|
||||
|
||||
```go
|
||||
// Default - uses mDNS automatically
|
||||
svc := micro.NewService("myservice")
|
||||
svc := micro.NewService(micro.Name("myservice"))
|
||||
|
||||
// Production - swap to Consul
|
||||
reg := consul.NewConsulRegistry()
|
||||
svc := micro.NewService("myservice",
|
||||
svc := micro.NewService(
|
||||
micro.Name("myservice"),
|
||||
micro.Registry(reg),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -29,7 +29,7 @@ Implement **progressive configuration** where:
|
||||
|
||||
### Level 1: Zero Config (Development)
|
||||
```go
|
||||
svc := micro.NewService("hello")
|
||||
svc := micro.NewService(micro.Name("hello"))
|
||||
svc.Run()
|
||||
```
|
||||
|
||||
@@ -62,7 +62,8 @@ b := nats.NewNatsBroker(
|
||||
nats.DrainConnection(),
|
||||
)
|
||||
|
||||
svc := micro.NewService("myservice",
|
||||
svc := micro.NewService(
|
||||
micro.Name("myservice"),
|
||||
micro.Version("1.2.3"),
|
||||
micro.Registry(reg),
|
||||
micro.Broker(b),
|
||||
|
||||
@@ -38,7 +38,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("publisher")
|
||||
service := micro.NewService()
|
||||
service.Init()
|
||||
|
||||
// Publish a message
|
||||
@@ -73,7 +73,7 @@ import (
|
||||
|
||||
func main() {
|
||||
b := bnats.NewNatsBroker()
|
||||
svc := micro.NewService("publisher", micro.Broker(b))
|
||||
svc := micro.NewService(micro.Broker(b))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -88,7 +88,7 @@ import (
|
||||
|
||||
func main() {
|
||||
b := rabbitmq.NewBroker()
|
||||
svc := micro.NewService("publisher", micro.Broker(b))
|
||||
svc := micro.NewService(micro.Broker(b))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
|
||||
@@ -35,7 +35,8 @@ func (g *Greeter) Hello(ctx context.Context, req *struct{}, rsp *struct{Msg stri
|
||||
}
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("greeter",
|
||||
service := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
)
|
||||
service.Init()
|
||||
micro.RegisterHandler(service.Server(), new(Greeter))
|
||||
|
||||
@@ -53,7 +53,8 @@ No code changes required. The framework internally wires the selected implementa
|
||||
## Equivalent Code Configuration
|
||||
|
||||
```go
|
||||
service := micro.NewService("helloworld",
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
micro.Broker(nats.NewBroker()),
|
||||
micro.Transport(natstransport.NewTransport()),
|
||||
micro.Registry(consul.NewRegistry(registry.Addrs("127.0.0.1:8500"))),
|
||||
|
||||
@@ -54,7 +54,8 @@ curl -XPOST \
|
||||
Set a fixed address:
|
||||
|
||||
```go
|
||||
svc := micro.NewService("helloworld",
|
||||
svc := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
micro.Address(":8080"),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
func main() {
|
||||
b := bnats.NewNatsBroker()
|
||||
svc := micro.NewService("nats-pubsub", micro.Broker(b))
|
||||
svc := micro.NewService(micro.Broker(b))
|
||||
svc.Init()
|
||||
|
||||
// subscribe
|
||||
|
||||
@@ -79,7 +79,8 @@ func main() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := micro.NewService("users",
|
||||
svc := micro.NewService(
|
||||
micro.Name("users"),
|
||||
micro.Version("1.0.0"),
|
||||
)
|
||||
|
||||
@@ -171,7 +172,8 @@ func main() {
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
svc := micro.NewService("orders",
|
||||
svc := micro.NewService(
|
||||
micro.Name("orders"),
|
||||
micro.Version("1.0.0"),
|
||||
)
|
||||
|
||||
@@ -252,7 +254,8 @@ func (g *Gateway) CreateOrder(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
|
||||
func main() {
|
||||
svc := micro.NewService("api.gateway",
|
||||
svc := micro.NewService(
|
||||
micro.Name("api.gateway"),
|
||||
)
|
||||
svc.Init()
|
||||
|
||||
|
||||
@@ -34,7 +34,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
svc := micro.NewService("myservice",
|
||||
svc := micro.NewService(
|
||||
micro.Name("myservice"),
|
||||
micro.BeforeStop(func() error {
|
||||
logger.Info("Service stopping, running cleanup...")
|
||||
return cleanup()
|
||||
@@ -232,7 +233,8 @@ func main() {
|
||||
app.AddWorker(&Worker{name: "cleanup"})
|
||||
app.AddWorker(&Worker{name: "metrics"})
|
||||
|
||||
svc := micro.NewService("myservice",
|
||||
svc := micro.NewService(
|
||||
micro.Name("myservice"),
|
||||
micro.BeforeStop(func() error {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
func main() {
|
||||
reg := consul.NewConsulRegistry()
|
||||
svc := micro.NewService("consul-registry", micro.Registry(reg))
|
||||
svc := micro.NewService(micro.Registry(reg))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
func main() {
|
||||
st := postgres.NewStore()
|
||||
svc := micro.NewService("postgres-store", micro.Store(st))
|
||||
svc := micro.NewService(micro.Store(st))
|
||||
svc.Init()
|
||||
|
||||
_ = store.Write(&store.Record{Key: "foo", Value: []byte("bar")})
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
|
||||
func main() {
|
||||
t := tnats.NewTransport()
|
||||
svc := micro.NewService("nats-transport", micro.Transport(t))
|
||||
svc := micro.NewService(micro.Transport(t))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
|
||||
@@ -71,5 +71,4 @@ harness. When that loop has to touch production, you do.
|
||||
- [Agent Loops](agent-loops.html) — run-until-done, with a ceiling
|
||||
- [Plan & Delegate](plan-delegate.html)
|
||||
- [Agent Guardrails](agent-guardrails.html)
|
||||
- [Provider Conformance](provider-conformance.html) — verified provider behavior
|
||||
- [Roadmap](/docs/roadmap.html)
|
||||
|
||||
@@ -29,8 +29,9 @@ runtime tooling and docs can report what is actually available after blank
|
||||
imports are linked in:
|
||||
|
||||
```go
|
||||
for _, row := range ai.CapabilityRows() {
|
||||
fmt.Printf("%s: chat=%t image=%t video=%t\n", row.Provider, row.Model, row.Image, row.Video)
|
||||
matrix := ai.CapabilityMatrix()
|
||||
for provider, caps := range matrix {
|
||||
fmt.Printf("%s: chat=%t image=%t video=%t\n", provider, caps.Model, caps.Image, caps.Video)
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ func (s *MyService) DoThing(ctx context.Context, req *Request, rsp *Response) er
|
||||
}
|
||||
|
||||
func main() {
|
||||
svc := micro.NewService("myservice")
|
||||
svc := micro.NewService(micro.Name("myservice"))
|
||||
svc.Init()
|
||||
svc.Handle(new(MyService))
|
||||
svc.Run()
|
||||
@@ -140,7 +140,7 @@ import (
|
||||
grpcClient "go-micro.dev/v6/client/grpc"
|
||||
)
|
||||
|
||||
svc := micro.NewService("myservice",
|
||||
svc := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
)
|
||||
@@ -234,11 +234,11 @@ mesh / runtime and let ADK (or any A2A agent) plug into it.
|
||||
**Go Micro**: Built-in with plugins
|
||||
```go
|
||||
// Zero-config for dev
|
||||
svc := micro.NewService("myservice")
|
||||
svc := micro.NewService(micro.Name("myservice"))
|
||||
|
||||
// Consul for production
|
||||
reg := consul.NewRegistry()
|
||||
svc := micro.NewService("myservice", micro.Registry(reg))
|
||||
svc := micro.NewService(micro.Registry(reg))
|
||||
```
|
||||
|
||||
**go-kit**: Bring your own
|
||||
|
||||
@@ -19,7 +19,8 @@ The gRPC **transport** uses the gRPC protocol as a communication layer, similar
|
||||
import "go-micro.dev/v6/transport/grpc"
|
||||
|
||||
t := grpc.NewTransport()
|
||||
service := micro.NewService("helloworld",
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"),
|
||||
micro.Transport(t),
|
||||
)
|
||||
```
|
||||
@@ -41,12 +42,15 @@ import (
|
||||
grpcClient "go-micro.dev/v6/client/grpc"
|
||||
)
|
||||
|
||||
service := micro.NewService("helloworld",
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()), // Server must come before Name
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
micro.Name("helloworld"),
|
||||
)
|
||||
```
|
||||
|
||||
> **Important**: The `micro.Server()` option must be specified **before** `micro.Name()`. This is because `micro.Name()` sets the name on the current server, and if `micro.Server()` comes after, it replaces the server with a new one that has no name set.
|
||||
|
||||
## When to Use Which
|
||||
|
||||
| Use Case | Solution |
|
||||
@@ -119,8 +123,9 @@ func (s *Say) Hello(ctx context.Context, req *pb.Request, rsp *pb.Response) erro
|
||||
func main() {
|
||||
// Create service with gRPC server for native gRPC compatibility
|
||||
// Note: Server must be set before Name to ensure the name is applied to the gRPC server
|
||||
service := micro.NewService("helloworld",
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
micro.Name("helloworld"),
|
||||
micro.Address(":8080"),
|
||||
)
|
||||
|
||||
@@ -153,8 +158,9 @@ import (
|
||||
|
||||
func main() {
|
||||
// Create service with gRPC client
|
||||
service := micro.NewService("helloworld.client",
|
||||
service := micro.NewService(
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
micro.Name("helloworld.client"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
@@ -201,9 +207,10 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("helloworld",
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()), // Server first
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
micro.Name("helloworld"), // Name after Server
|
||||
micro.Address(":8080"),
|
||||
)
|
||||
|
||||
@@ -232,7 +239,7 @@ ERROR:
|
||||
```go
|
||||
// Wrong - uses transport
|
||||
t := grpc.NewTransport()
|
||||
service := micro.NewService("helloworld",
|
||||
service := micro.NewService(
|
||||
micro.Transport(t),
|
||||
)
|
||||
```
|
||||
@@ -243,7 +250,7 @@ To:
|
||||
// Correct - uses server
|
||||
import grpcServer "go-micro.dev/v6/server/grpc"
|
||||
|
||||
service := micro.NewService("helloworld",
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
)
|
||||
```
|
||||
@@ -263,12 +270,36 @@ import "go-micro.dev/v6/server/grpc"
|
||||
import "go-micro.dev/v6/client/grpc"
|
||||
```
|
||||
|
||||
### Service Name vs Package Name
|
||||
### Option Ordering Issue
|
||||
|
||||
When creating a client to call another service, use the **service name** passed to `micro.NewService`, not the proto package name:
|
||||
If the gRPC server is working but your service has no name or is not being found in the registry:
|
||||
|
||||
**Cause**: The `micro.Server()` option is specified **after** `micro.Name()`.
|
||||
|
||||
When options are processed, `micro.Name()` sets the name on the current server. If `micro.Server()` comes later, it replaces the server with a new one that doesn't have the name set.
|
||||
|
||||
**Solution**: Always specify `micro.Server()` **before** `micro.Name()`:
|
||||
|
||||
```go
|
||||
// If the server was started with micro.NewService("helloworld", ...)
|
||||
// Wrong - server replaces the one with the name set
|
||||
service := micro.NewService(
|
||||
micro.Name("helloworld"), // Sets name on default server
|
||||
micro.Server(grpcServer.NewServer()), // Replaces server, name is lost!
|
||||
)
|
||||
|
||||
// Correct - name is set on the gRPC server
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()), // Set server first
|
||||
micro.Name("helloworld"), // Name is now applied to gRPC server
|
||||
)
|
||||
```
|
||||
|
||||
### Service Name vs Package Name
|
||||
|
||||
When creating a client to call another service, use the **service name** (set via `micro.Name()`), not the proto package name:
|
||||
|
||||
```go
|
||||
// If the server was started with micro.Name("helloworld")
|
||||
sayService := pb.NewSayService("helloworld", service.Client()) // Use service name
|
||||
|
||||
// NOT the package name from the proto file
|
||||
|
||||
@@ -319,7 +319,8 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("tasks",
|
||||
service := micro.NewService(
|
||||
micro.Name("tasks"),
|
||||
micro.Address(":8081"),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
@@ -128,7 +128,8 @@ func (s *Greeter) SayHello(ctx context.Context, req *pb.HelloRequest, rsp *pb.He
|
||||
}
|
||||
|
||||
func main() {
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
)
|
||||
svc.Init()
|
||||
|
||||
@@ -158,7 +159,7 @@ rsp, err := client.SayHello(context.Background(), &pb.HelloRequest{Name: "John"}
|
||||
|
||||
**Go Micro client:**
|
||||
```go
|
||||
svc := micro.NewService("client")
|
||||
svc := micro.NewService(micro.Name("client"))
|
||||
svc.Init()
|
||||
|
||||
client := pb.NewGreeterService("greeter", svc.Client())
|
||||
@@ -184,7 +185,8 @@ import (
|
||||
grpcserver "go-micro.dev/v6/server/grpc"
|
||||
)
|
||||
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.Client(grpcclient.NewClient()),
|
||||
micro.Server(grpcserver.NewServer()),
|
||||
)
|
||||
@@ -266,7 +268,8 @@ defer client.Agent().ServiceDeregister("greeter-1")
|
||||
import "go-micro.dev/v6/registry/consul"
|
||||
|
||||
reg := consul.NewConsulRegistry()
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.Registry(reg),
|
||||
)
|
||||
|
||||
@@ -290,7 +293,7 @@ svc.Run()
|
||||
import "go-micro.dev/v6/selector"
|
||||
|
||||
// Client-side load balancing built-in
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Selector(selector.NewSelector(
|
||||
selector.SetStrategy(selector.RoundRobin),
|
||||
)),
|
||||
@@ -359,10 +362,11 @@ lis, _ := net.Listen("tcp", ":50051")
|
||||
**Go Micro**: Automatic or explicit
|
||||
```go
|
||||
// Let Go Micro choose
|
||||
svc := micro.NewService("greeter")
|
||||
svc := micro.NewService(micro.Name("greeter"))
|
||||
|
||||
// Or specify
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Name("greeter"),
|
||||
micro.Address(":50051"),
|
||||
)
|
||||
```
|
||||
@@ -386,7 +390,7 @@ Ensure both use protobuf:
|
||||
```go
|
||||
import "go-micro.dev/v6/codec/proto"
|
||||
|
||||
svc := micro.NewService("greeter",
|
||||
svc := micro.NewService(
|
||||
micro.Codec("application/protobuf", proto.Marshaler{}),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1,96 +0,0 @@
|
||||
---
|
||||
layout: default
|
||||
---
|
||||
|
||||
# Provider Conformance Matrix
|
||||
|
||||
Go Micro treats model providers as interchangeable pieces of the same agent
|
||||
harness: services expose tools, agents reason over them, and workflows stitch the
|
||||
work together. The conformance harness keeps that promise honest by running the
|
||||
same deterministic services → agents → workflows scenarios against every
|
||||
configured provider.
|
||||
|
||||
The live harness is in `internal/harness/provider-conformance`. It skips
|
||||
providers without API keys by default, so it is safe to run locally, and it fails
|
||||
when any configured provider breaks the shared contract.
|
||||
|
||||
```sh
|
||||
go run ./internal/harness/provider-conformance
|
||||
```
|
||||
|
||||
For a no-key smoke test of the same harness wiring, run the mock provider:
|
||||
|
||||
```sh
|
||||
go run ./internal/harness/provider-conformance -providers mock
|
||||
```
|
||||
|
||||
## Status legend
|
||||
|
||||
| Status | Meaning |
|
||||
| --- | --- |
|
||||
| ✅ Verified | Covered by the provider-conformance harness for configured live providers. |
|
||||
| ⚠️ Unverified | Implemented in the public API, but not yet exercised by provider conformance. |
|
||||
| — Unsupported | Not exposed by that provider integration today. |
|
||||
|
||||
## Harness coverage by capability
|
||||
|
||||
These rows describe what the conformance harness verifies today. A provider is
|
||||
considered conformant when the configured-key run passes all selected harnesses.
|
||||
|
||||
| Capability | Harness coverage | Notes |
|
||||
| --- | --- | --- |
|
||||
| Simple generation | ✅ Verified | Each harness asks the provider to produce an agent response through `ai.Model`. |
|
||||
| Service tool calls | ✅ Verified | Harness services are discovered and invoked as model-selected tools. |
|
||||
| Multi-step tool use | ✅ Verified | The `universe` and `plan-delegate` harnesses require more than one service/tool action. |
|
||||
| `plan` | ✅ Verified | `plan-delegate` verifies that the conductor agent stores a plan in scoped state. |
|
||||
| `delegate` | ✅ Verified | `plan-delegate` verifies agent-to-agent delegation over real RPC. |
|
||||
| Guardrail/stop behavior | ✅ Verified | `universe` runs with guardrails enabled and asserts the guarded path completes. |
|
||||
| Streaming | ⚠️ Unverified | `ai.Model.Stream` exists on the interface, but end-to-end streaming conformance is a roadmap item. |
|
||||
| Structured errors | ⚠️ Unverified | Error handling is covered by normal test suites, but provider conformance does not yet compare structured provider errors. |
|
||||
|
||||
## Provider capability matrix
|
||||
|
||||
This matrix combines the registered provider interfaces with the conformance
|
||||
coverage above. The chat/text column is the harness path: when the provider has a
|
||||
configured key, the conformance command exercises the verified rows in the
|
||||
previous section.
|
||||
|
||||
| Provider | Chat/text agent harness | Image | Video | Streaming | Structured errors |
|
||||
| --- | --- | --- | --- | --- | --- |
|
||||
| `anthropic` | ✅ Verified when configured | — Unsupported | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `openai` | ✅ Verified when configured | ✅ Registered | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `gemini` | ✅ Verified when configured | — Unsupported | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `groq` | ✅ Verified when configured | — Unsupported | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `mistral` | ✅ Verified when configured | — Unsupported | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `together` | ✅ Verified when configured | — Unsupported | — Unsupported | ⚠️ Unverified | ⚠️ Unverified |
|
||||
| `atlascloud` | ✅ Verified when configured | ✅ Registered | ✅ Registered | ⚠️ Unverified | ⚠️ Unverified |
|
||||
|
||||
## Running a focused check
|
||||
|
||||
Use `-providers` to select a provider and `-harnesses` to narrow the scenario:
|
||||
|
||||
```sh
|
||||
go run ./internal/harness/provider-conformance \
|
||||
-providers openai,anthropic \
|
||||
-harnesses agent-flow,plan-delegate
|
||||
```
|
||||
|
||||
By default missing live-provider keys are reported as skips. Add
|
||||
`-require-configured` in CI when a selected provider must be present:
|
||||
|
||||
```sh
|
||||
go run ./internal/harness/provider-conformance \
|
||||
-providers openai \
|
||||
-require-configured
|
||||
```
|
||||
|
||||
The command also prints the registered model, image, and video provider
|
||||
capabilities before running conformance. Disable that with `-capabilities=false`
|
||||
when you only want pass/fail output.
|
||||
|
||||
## Related docs
|
||||
|
||||
- [The Agent Harness](agent-harness.html)
|
||||
- [Agents and Workflows](agents-and-workflows.html)
|
||||
- [AI Provider Guide](ai-provider-guide.html)
|
||||
- [Roadmap](/docs/roadmap.html)
|
||||
@@ -98,7 +98,7 @@ Choose based on your deployment:
|
||||
import "go-micro.dev/v6/server/grpc"
|
||||
|
||||
// Use gRPC for better performance
|
||||
service := micro.NewService("performance-example",
|
||||
service := micro.NewService(
|
||||
micro.Server(grpc.NewServer()),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
func main() {
|
||||
reg := consul.NewConsulRegistry()
|
||||
svc := micro.NewService("plugin-example",
|
||||
svc := micro.NewService(
|
||||
micro.Registry(reg),
|
||||
)
|
||||
svc.Init()
|
||||
@@ -43,7 +43,7 @@ import (
|
||||
|
||||
func main() {
|
||||
reg := etcd.NewRegistry()
|
||||
svc := micro.NewService("plugin-example", micro.Registry(reg))
|
||||
svc := micro.NewService(micro.Registry(reg))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -60,7 +60,7 @@ import (
|
||||
|
||||
func main() {
|
||||
b := bnats.NewNatsBroker()
|
||||
svc := micro.NewService("plugin-example", micro.Broker(b))
|
||||
svc := micro.NewService(micro.Broker(b))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -75,7 +75,7 @@ import (
|
||||
|
||||
func main() {
|
||||
b := rabbitmq.NewBroker()
|
||||
svc := micro.NewService("plugin-example", micro.Broker(b))
|
||||
svc := micro.NewService(micro.Broker(b))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -90,7 +90,7 @@ import (
|
||||
|
||||
func main() {
|
||||
t := tnats.NewTransport()
|
||||
svc := micro.NewService("plugin-example", micro.Transport(t))
|
||||
svc := micro.NewService(micro.Transport(t))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -108,7 +108,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
svc := micro.NewService("plugin-example",
|
||||
svc := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
)
|
||||
@@ -130,7 +130,7 @@ import (
|
||||
|
||||
func main() {
|
||||
st := postgres.NewStore()
|
||||
svc := micro.NewService("plugin-example", micro.Store(st))
|
||||
svc := micro.NewService(micro.Store(st))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -145,7 +145,7 @@ import (
|
||||
|
||||
func main() {
|
||||
st := natsjskv.NewStore()
|
||||
svc := micro.NewService("plugin-example", micro.Store(st))
|
||||
svc := micro.NewService(micro.Store(st))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ import (
|
||||
|
||||
func main() {
|
||||
reg := consul.NewRegistry()
|
||||
service := micro.NewService("registry-example",
|
||||
service := micro.NewService(
|
||||
micro.Registry(reg),
|
||||
)
|
||||
service.Init()
|
||||
|
||||
@@ -36,7 +36,7 @@ import (
|
||||
)
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("store-example")
|
||||
service := micro.NewService()
|
||||
service.Init()
|
||||
|
||||
// Write a record
|
||||
@@ -64,7 +64,7 @@ import (
|
||||
|
||||
func main() {
|
||||
st := postgres.NewStore()
|
||||
svc := micro.NewService("store-example", micro.Store(st))
|
||||
svc := micro.NewService(micro.Store(st))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
@@ -79,7 +79,7 @@ import (
|
||||
|
||||
func main() {
|
||||
st := natsjskv.NewStore()
|
||||
svc := micro.NewService("store-example", micro.Store(st))
|
||||
svc := micro.NewService(micro.Store(st))
|
||||
svc.Init()
|
||||
svc.Run()
|
||||
}
|
||||
|
||||
@@ -31,9 +31,11 @@ import (
|
||||
grpcClient "go-micro.dev/v6/client/grpc"
|
||||
)
|
||||
|
||||
service := micro.NewService("myservice",
|
||||
// Important: Server must be specified before Name
|
||||
service := micro.NewService(
|
||||
micro.Server(grpcServer.NewServer()),
|
||||
micro.Client(grpcClient.NewClient()),
|
||||
micro.Name("myservice"),
|
||||
)
|
||||
```
|
||||
|
||||
@@ -57,7 +59,7 @@ import (
|
||||
|
||||
func main() {
|
||||
t := grpc.NewTransport()
|
||||
service := micro.NewService("transport-example",
|
||||
service := micro.NewService(
|
||||
micro.Transport(t),
|
||||
)
|
||||
service.Init()
|
||||
@@ -74,7 +76,7 @@ import (
|
||||
|
||||
func main() {
|
||||
t := tnats.NewTransport()
|
||||
service := micro.NewService("transport-example", micro.Transport(t))
|
||||
service := micro.NewService(micro.Transport(t))
|
||||
service.Init()
|
||||
service.Run()
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"go-micro.dev/v6/server"
|
||||
"go-micro.dev/v6/service"
|
||||
"go-micro.dev/v6/store"
|
||||
"go.opentelemetry.io/otel/trace"
|
||||
)
|
||||
|
||||
type serviceKey struct{}
|
||||
@@ -202,9 +201,6 @@ func FlowWithCheckpoint(c Checkpoint) FlowOption { return flow.WithCheckpoint(c)
|
||||
// are always kept). Default: retain all.
|
||||
func FlowDeleteOnSuccess() FlowOption { return flow.DeleteOnSuccess() }
|
||||
|
||||
// FlowTraceProvider enables OpenTelemetry spans for stepped flow runs and steps.
|
||||
func FlowTraceProvider(tp trace.TracerProvider) FlowOption { return flow.TraceProvider(tp) }
|
||||
|
||||
// FlowCall is a step action: an RPC to a service endpoint, sending the
|
||||
// state data as the request and storing the response.
|
||||
func FlowCall(service, endpoint string) FlowStepFunc { return flow.Call(service, endpoint) }
|
||||
|
||||
Reference in New Issue
Block a user