Compare commits

..

1 Commits

Author SHA1 Message Date
Codex 80102ae526 docs(priorities): refresh planner queue for 3900
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-04 09:05:43 +00:00
13 changed files with 36 additions and 397 deletions
+3 -4
View File
@@ -21,10 +21,9 @@ changes, architectural rewrites. Those go to the human.
## Work queue (ranked)
1. **Make AtlasCloud conformance deterministically request the echo tool** ([#3924](https://github.com/micro/go-micro/issues/3924)) — #3926 shipped the previous plan/delegate notify wait fix, so the queue should drop the completed #3918 item. The freshest live harness signal now fails even earlier: AtlasCloud/minimax-m3 completes without invoking the required `conformance_echo` tool. This is the highest Now-phase item because the getting-started and 0→hero contract depends on providers actually using service tools, not just returning prose, and the CI gate needs an actionable deterministic failure path.
2. **Make AtlasCloud conformance deterministically exercise guarded delegation** ([#3917](https://github.com/micro/go-micro/issues/3917)) — after the echo-tool path is stable, the same live provider must attempt the blocked delegate path so guardrails are proven under a real model, not only mocks. This keeps plan/delegate safety provider-portable without public API changes and protects the services → agents → workflows story from drifting at the delegation seam.
3. **Broaden provider streaming and keep chat/A2A streaming end to end** ([#3903](https://github.com/micro/go-micro/issues/3903)) — once the live gate is stable, streaming is the highest developer-visible Next-phase seam. Real chat and long-running A2A tasks need token streaming to stay coherent from provider → `ai.Stream``micro chat` → A2A `message/stream`, with mock/default CI coverage plus key-gated live provider checks and safe fallback for non-streaming providers.
4. **Trace agent runs as OpenTelemetry spans** ([#3908](https://github.com/micro/go-micro/issues/3908)) — the blog/README/roadmap story promises an operable harness, and the developer on-ramp now includes chat, inspect, and run-history checkpoints. The next observability gap is production-grade trace correlation for `RunInfo`: steps, tool calls, delegation, status, durations, and failures should be visible as spans while defaulting to no-op when tracing is not configured.
1. **Add cross-provider agent conformance** ([#3901](https://github.com/micro/go-micro/issues/3901)) — #3895 closed the provider deadline/retry guidance gap and #3899 closed the AP2 foundation, leaving no open `codex` PRs or issues in flight. The highest-value Now-phase gap is therefore battle-testing the same first-agent behavior across providers: tool calling, multi-step runs, plan/delegate, and guardrails should fail loudly by provider instead of drifting behind a single happy-path backend. Keep default CI no-secret friendly with mock coverage and key-gated live providers.
2. **Checkpoint and resume durable agent runs** ([#3902](https://github.com/micro/go-micro/issues/3902)) — adoption now has install, scaffold, run, chat, inspect/history, and 0→hero coverage, but the lived harness story still has an asymmetry: flows checkpoint and resume while long agent runs do not. This is the top Next-phase agentic-depth gap because it makes services → agents → workflows feel like one runtime under interruption and deploy/restart conditions. Keep it additive; do not break the public agent API.
3. **Broaden provider streaming and keep chat/A2A streaming end to end** ([#3903](https://github.com/micro/go-micro/issues/3903)) — after conformance and durability, streaming is the next developer-visible seam in the inner loop. Real chat and long-running A2A tasks need token streaming to stay coherent from provider → `ai.Stream``micro chat` → A2A `message/stream`, with mock/default CI coverage plus key-gated live provider checks and safe fallback for non-streaming providers.
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
architecture-review pass._
+4 -5
View File
@@ -87,15 +87,14 @@ make harness
After install and the first `micro new`/`micro run` smoke check, take the
walkable agent path in this order:
1. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
2. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
1. [No-secret first-agent transcript](internal/website/docs/guides/no-secret-first-agent.md) — run the
maintained support agent with a mock model and see services → agents → workflows succeed without a key.
3. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
2. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
service-backed agent and talk to it with `micro chat`.
4. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
3. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
`micro agent inspect`, run history, memory, and provider checks when the first
conversation does something unexpected.
5. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
4. [0→hero Reference](internal/website/docs/guides/zero-to-hero.md) — complete the
services → agents → workflows loop with scaffold, run, chat, inspect, flow
history, and deploy dry-run commands that match the maintained harness.
+18 -136
View File
@@ -67,45 +67,30 @@ func runAgentConformanceScenario(t *testing.T, provider conformanceProvider) {
}
} else {
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
if err := validateConformanceRequest(req, opts); err != nil {
return nil, err
if req.Prompt == "" {
return nil, errors.New("missing prompt")
}
plan := opts.ToolHandler(ctx, ai.ToolCall{
ID: "fake-plan-1",
Name: "plan",
Input: map[string]any{"steps": []map[string]any{
{"description": "call conformance_echo", "status": "pending"},
{"description": "attempt guarded delegate", "status": "pending"},
}},
})
echo := opts.ToolHandler(ctx, ai.ToolCall{
if len(req.Messages) == 0 || req.Messages[len(req.Messages)-1].Role != "user" {
return nil, fmt.Errorf("missing user history: %+v", req.Messages)
}
if len(req.Tools) == 0 {
return nil, errors.New("missing tools")
}
if opts.ToolHandler == nil {
return nil, errors.New("missing tool handler")
}
res := opts.ToolHandler(ctx, ai.ToolCall{
ID: "fake-call-1",
Name: "conformance_echo",
Input: map[string]any{"value": "agent-conformance"},
})
delegate := opts.ToolHandler(ctx, ai.ToolCall{
ID: "fake-delegate-1",
Name: "delegate",
Input: map[string]any{"task": "summarize the conformance marker", "to": "blocked-reviewer"},
})
if plan.Content == "" {
return nil, errors.New("empty plan result")
}
if echo.Content == "" {
if res.Content == "" {
return nil, errors.New("empty tool result")
}
if delegate.Refused != ai.RefusedApproval {
return nil, fmt.Errorf("delegate refusal = %q, want %q", delegate.Refused, ai.RefusedApproval)
}
return &ai.Response{
Reply: "planned, called conformance_echo, and handled guarded delegate refusal",
Answer: echo.Content + " " + delegate.Content,
ToolCalls: []ai.ToolCall{
{ID: "fake-plan-1", Name: "plan", Input: map[string]any{}},
{ID: "fake-call-1", Name: "conformance_echo", Input: map[string]any{"value": "agent-conformance"}, Result: echo.Content},
{ID: "fake-delegate-1", Name: "delegate", Input: map[string]any{"task": "summarize the conformance marker", "to": "blocked-reviewer"}, Error: delegate.Content},
},
Reply: "used conformance_echo",
Answer: res.Content,
ToolCalls: []ai.ToolCall{{ID: "fake-call-1", Name: "conformance_echo", Input: map[string]any{"value": "agent-conformance"}, Result: res.Content}},
}, nil
}
defer func() { fakeGen = nil }()
@@ -113,23 +98,15 @@ func runAgentConformanceScenario(t *testing.T, provider conformanceProvider) {
var sawTool bool
var sawRunInfo bool
var sawBlockedDelegate bool
agentOpts := []Option{
Name("conformance-" + provider.name),
Provider(provider.name),
APIKey(os.Getenv(provider.key)),
Prompt("You are a conformance test agent. Create a short plan, use conformance_echo exactly once with input {\"value\":\"agent-conformance\"}, then attempt to delegate a summary to blocked-reviewer. If the delegate is refused, explain the refusal and answer with the echo result."),
Prompt("You are a conformance test agent. Use the conformance_echo tool exactly once with input {\"value\":\"agent-conformance\"}, then answer with the tool result."),
WithRegistry(registry.NewMemoryRegistry()),
WithStore(store.NewMemoryStore()),
WithMemory(NewInMemory(8)),
ModelCallTimeout(45 * time.Second),
ApproveTool(func(tool string, input map[string]any) (bool, string) {
if tool == "delegate" {
sawBlockedDelegate = true
return false, "cross-provider conformance blocks delegate side effects"
}
return true, ""
}),
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) {
@@ -155,7 +132,7 @@ func runAgentConformanceScenario(t *testing.T, provider conformanceProvider) {
}
a := New(agentOpts...)
resp, err := askWithConformanceToolRetry(context.Background(), a, "Run the provider conformance check.", &sawTool)
resp, err := a.Ask(context.Background(), "Run the provider conformance check.")
if err != nil {
t.Fatalf("Ask: %v", err)
}
@@ -171,51 +148,11 @@ func runAgentConformanceScenario(t *testing.T, provider conformanceProvider) {
if !sawRunInfo {
t.Fatal("tool did not receive RunInfo")
}
if !sawBlockedDelegate {
t.Fatal("provider did not exercise the guarded delegate path")
}
if !strings.Contains(resp.Reply, "agent-conformance-ok") && !strings.Contains(resp.Reply, "agent-conformance") {
t.Fatalf("reply %q does not include conformance marker", resp.Reply)
}
}
func askWithConformanceToolRetry(ctx context.Context, a Agent, initialPrompt string, sawTool *bool) (*Response, error) {
const maxAttempts = 2
prompt := initialPrompt
var resp *Response
for attempt := 1; attempt <= maxAttempts; attempt++ {
var err error
resp, err = a.Ask(ctx, prompt)
if err != nil {
return nil, err
}
if sawTool != nil && *sawTool {
return resp, nil
}
if attempt == maxAttempts {
break
}
prompt = "The previous response did not call the required conformance_echo tool. Retry the same conformance check now: you must call conformance_echo exactly once with input {\"value\":\"agent-conformance\"} before any final answer, then include the tool result marker in the final answer."
}
return resp, nil
}
func validateConformanceRequest(req *ai.Request, opts ai.Options) error {
if req.Prompt == "" {
return errors.New("missing prompt")
}
if len(req.Messages) == 0 || req.Messages[len(req.Messages)-1].Role != "user" {
return fmt.Errorf("missing user history: %+v", req.Messages)
}
if len(req.Tools) == 0 {
return errors.New("missing tools")
}
if opts.ToolHandler == nil {
return errors.New("missing tool handler")
}
return nil
}
func TestAgentProviderConformanceFakeError(t *testing.T) {
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
return nil, errors.New("conformance provider failure")
@@ -235,61 +172,6 @@ func TestAgentProviderConformanceFakeError(t *testing.T) {
}
}
func TestAgentProviderConformanceRetriesMissingTool(t *testing.T) {
var attempts int
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
attempts++
if err := validateConformanceRequest(req, opts); err != nil {
return nil, err
}
if attempts == 1 {
return &ai.Response{Reply: "I can confirm agent-conformance in prose only."}, nil
}
echo := opts.ToolHandler(ctx, ai.ToolCall{
ID: "fake-call-1",
Name: "conformance_echo",
Input: map[string]any{"value": "agent-conformance"},
})
return &ai.Response{
Reply: "called conformance_echo",
Answer: echo.Content,
ToolCalls: []ai.ToolCall{
{ID: "fake-call-1", Name: "conformance_echo", Input: map[string]any{"value": "agent-conformance"}, Result: echo.Content},
},
}, nil
}
defer func() { fakeGen = nil }()
var sawTool bool
a := New(
Name("conformance-retry"),
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
return `{"marker":"agent-conformance-ok"}`, nil
}),
)
resp, err := askWithConformanceToolRetry(context.Background(), a, "Run the provider conformance check.", &sawTool)
if err != nil {
t.Fatalf("Ask: %v", err)
}
if attempts != 2 {
t.Fatalf("attempts = %d, want retry after missing tool", attempts)
}
if !sawTool {
t.Fatal("retry did not execute conformance_echo")
}
if !strings.Contains(resp.Reply, "agent-conformance-ok") {
t.Fatalf("Reply = %q, want tool result marker", resp.Reply)
}
}
func TestAgentExecutesProviderTextToolCallFallback(t *testing.T) {
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
if opts.ToolHandler == nil {
-6
View File
@@ -103,8 +103,6 @@ type RunSummary struct {
DurationMS int64 `json:"duration_ms,omitempty"`
Events int `json:"events"`
Status string `json:"status,omitempty"`
Checkpoint string `json:"checkpoint,omitempty"`
Stage string `json:"stage,omitempty"`
LastKind string `json:"last_kind,omitempty"`
LastError string `json:"last_error,omitempty"`
LastErrorKind string `json:"last_error_kind,omitempty"`
@@ -582,10 +580,6 @@ func ListRunSummariesWithOptions(s store.Store, agentName string, opts RunListOp
if e.SpanID != "" {
summary.SpanID = e.SpanID
}
if e.Kind == "checkpoint" {
summary.Checkpoint = e.Status
summary.Stage = e.Name
}
if e.Error != "" {
summary.LastError = e.Error
}
+2 -3
View File
@@ -500,8 +500,7 @@ func TestListRunSummaries(t *testing.T) {
{Time: time.Unix(0, 1), RunID: "run-a", Agent: "runner", TraceID: "trace-a", SpanID: "span-a", Kind: "run", Name: "first"},
{Time: time.Unix(0, 2), RunID: "run-a", Agent: "runner", Kind: "tool", Name: "probe"},
{Time: time.Unix(0, 3), RunID: "run-b", Agent: "runner", ParentID: "parent", Kind: "run", Name: "second"},
{Time: time.Unix(0, 4), RunID: "run-b", Agent: "runner", ParentID: "parent", Kind: "checkpoint", Name: "ask", Status: "failed"},
{Time: time.Unix(0, 5), RunID: "run-b", Agent: "runner", ParentID: "parent", Kind: "error", Error: "context deadline exceeded", ErrorKind: string(ai.ErrorKindTimeout)},
{Time: time.Unix(0, 4), RunID: "run-b", Agent: "runner", ParentID: "parent", Kind: "error", Error: "context deadline exceeded", ErrorKind: string(ai.ErrorKindTimeout)},
}
for _, e := range events {
b, err := json.Marshal(e)
@@ -524,7 +523,7 @@ func TestListRunSummaries(t *testing.T) {
if got[0].RunID != "run-a" || got[0].TraceID != "trace-a" || got[0].SpanID != "span-a" || got[0].Events != 2 || got[0].Status != "running" || got[0].DurationMS != 0 || got[0].LastKind != "tool" || !got[0].UpdatedAt.Equal(time.Unix(0, 2)) {
t.Fatalf("unexpected run-a summary: %#v", got[0])
}
if got[1].RunID != "run-b" || got[1].ParentID != "parent" || got[1].Events != 3 || got[1].Status != "timeout" || got[1].DurationMS != 0 || got[1].LastKind != "error" || got[1].Checkpoint != "failed" || got[1].Stage != "ask" || got[1].LastError != "context deadline exceeded" || got[1].LastErrorKind != string(ai.ErrorKindTimeout) {
if got[1].RunID != "run-b" || got[1].ParentID != "parent" || got[1].Events != 2 || got[1].Status != "timeout" || got[1].DurationMS != 0 || got[1].LastKind != "error" || got[1].LastError != "context deadline exceeded" || got[1].LastErrorKind != string(ai.ErrorKindTimeout) {
t.Fatalf("unexpected run-b summary: %#v", got[1])
}
}
-21
View File
@@ -85,12 +85,6 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
fmt.Fprintf(w, " Agent %q runs\n", name)
for _, run := range runs {
fmt.Fprintf(w, " %s status=%s events=%d last=%s", run.RunID, run.Status, run.Events, run.LastKind)
if run.Checkpoint != "" {
fmt.Fprintf(w, " checkpoint=%s", run.Checkpoint)
}
if run.Stage != "" {
fmt.Fprintf(w, " stage=%s", run.Stage)
}
if run.LastError != "" {
fmt.Fprintf(w, " error=%q", run.LastError)
}
@@ -98,25 +92,10 @@ func writeAgentInspection(w io.Writer, name string, runs []goagent.RunSummary, a
fmt.Fprintf(w, " trace=%s", shortID(run.TraceID))
}
fmt.Fprintln(w)
if isResumableAgentRun(run) {
fmt.Fprintf(w, " resume: call micro.AgentResume(ctx, agent, %q) after recreating the agent with the same checkpoint store\n", run.RunID)
}
if run.Stage == "input-required" {
fmt.Fprintf(w, " input: call micro.AgentResumeInput(ctx, agent, %q, input) to continue the paused run\n", run.RunID)
}
}
return nil
}
func isResumableAgentRun(run goagent.RunSummary) bool {
switch run.Status {
case "running", "error", "failed", "refused":
return run.Checkpoint != "done" || run.Stage != ""
default:
return false
}
}
func inspectFlow(c *cli.Context) error {
name := c.Args().First()
if name == "" {
+2 -16
View File
@@ -11,27 +11,13 @@ import (
)
func TestWriteAgentInspectionIncludesActionableBreadcrumbs(t *testing.T) {
runs := []goagent.RunSummary{{RunID: "run-1", Status: "error", Events: 4, LastKind: "tool", LastError: "boom", TraceID: "1234567890abcdef", Checkpoint: "failed", Stage: "ask"}}
runs := []goagent.RunSummary{{RunID: "run-1", Status: "error", Events: 4, LastKind: "tool", LastError: "boom", TraceID: "1234567890abcdef"}}
var out bytes.Buffer
if err := writeAgentInspection(&out, "support", runs, false); err != nil {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=error", "events=4", "last=tool", "checkpoint=failed", "stage=ask", `error="boom"`, "trace=1234567890ab", `micro.AgentResume(ctx, agent, "run-1")`} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
}
}
func TestWriteAgentInspectionIncludesInputResumeBreadcrumb(t *testing.T) {
runs := []goagent.RunSummary{{RunID: "run-input", Status: "running", Events: 3, LastKind: "checkpoint", Checkpoint: "paused", Stage: "input-required"}}
var out bytes.Buffer
if err := writeAgentInspection(&out, "support", runs, false); err != nil {
t.Fatal(err)
}
got := out.String()
for _, want := range []string{"checkpoint=paused", "stage=input-required", `micro.AgentResumeInput(ctx, agent, "run-input", input)`} {
for _, want := range []string{"Agent \"support\" runs", "run-1", "status=error", "events=4", "last=tool", `error="boom"`, "trace=1234567890ab"} {
if !strings.Contains(got, want) {
t.Fatalf("output missing %q:\n%s", want, got)
}
+2 -5
View File
@@ -15,7 +15,7 @@ of reading the directories alphabetically.
| Step | Start here | What you learn | Next step |
|------|------------|----------------|-----------|
| 1. First service | [`hello-world`](./hello-world/) | Create and register a basic RPC service, add a handler, call it with a client, and expose health checks. | Move to [`agent-demo`](./agent-demo/) to see services used by an agent. |
| 2. First agent | [`first-agent`](./first-agent/) | Run the smallest service-backed agent with a deterministic mock model and no provider key. | Compare with [`agent-demo`](./agent-demo/) or the maintained 0-to-hero path in [`support`](./support/). |
| 2. First agent | [`agent-demo`](./agent-demo/) | Run a small project-management app with Projects, Tasks, and Team services plus an agent playground. | Compare with the maintained 0-to-hero path in [`support`](./support/). |
| 3. First workflow | [`support`](./support/) | Follow typed services into an agent chat loop, an event-driven `intake` flow, and an approval gate in one runnable reference. | Deepen the workflow model with [`flow-durable`](./flow-durable/). |
For the shortest AI-tooling bridge, the MCP path is
@@ -75,11 +75,8 @@ Docker Compose deployment with MCP gateway, Consul registry, and Jaeger tracing:
### 2. Agents — turn services into tool-using teammates
#### [first-agent](./first-agent/)
Smallest first agent: one notes service plus one scoped agent, backed by a deterministic mock model so `go run ./examples/first-agent` works without provider secrets.
#### [agent-demo](./agent-demo/)
A multi-service project management app with Projects,
Recommended first agent: a multi-service project management app with Projects,
Tasks, and Team services, seed data, and agent playground integration.
#### [agent-plan-delegate](./agent-plan-delegate/)
-38
View File
@@ -1,38 +0,0 @@
# First Agent
This is the smallest runnable service-backed agent in the repository. It sits
between `micro new helloworld` and the full [`examples/support`](../support/)
0→hero reference.
It runs with a deterministic mock model, so you do not need `ANTHROPIC_API_KEY`,
`OPENAI_API_KEY`, or any other provider secret.
```bash
go run ./examples/first-agent
```
Expected transcript:
```text
First agent (provider: mock, no API key)
> Summarize my next steps
[notes] listed starter notes
assistant: Your first agent read the notes service and found three steps: install the CLI, run a service, then chat with an agent.
✓ service-backed agent completed without provider secrets
```
## What it demonstrates
- `notes` is a normal Go Micro service with one RPC method.
- `assistant` is an agent scoped to that service via `agent.Services("notes")`.
- The mock model requests the service tool through the normal agent tool handler.
- The final answer proves the service → agent path without a live model key.
CI keeps this path runnable with:
```bash
go test ./examples/first-agent
```
After this, continue to [`examples/support`](../support/) for the full services →
agents → workflows lifecycle with a flow trigger and an approval gate.
-146
View File
@@ -1,146 +0,0 @@
// First Agent — the smallest runnable service-backed agent.
//
// Run:
//
// go run ./examples/first-agent
//
// It uses a deterministic mock model, so it needs no provider API key. The
// point is to show the first agent shape: a service exposes a tool, an agent
// discovers that service, the model asks to call the tool, and the agent returns
// a final answer.
package main
import (
"context"
"fmt"
"os"
"strings"
"time"
"go-micro.dev/v6/agent"
"go-micro.dev/v6/ai"
"go-micro.dev/v6/client"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/selector"
"go-micro.dev/v6/service"
"go-micro.dev/v6/store"
)
type ListNotesRequest struct{}
type ListNotesResponse struct {
Notes []string `json:"notes" description:"Notes the assistant can summarize"`
}
type NotesService struct{}
// List returns the starter notes the first agent can read.
// @example {}
func (s *NotesService) List(ctx context.Context, req *ListNotesRequest, rsp *ListNotesResponse) error {
rsp.Notes = []string{"Install the micro CLI", "Run a service", "Chat with an agent"}
fmt.Println(" [notes] listed starter notes")
return nil
}
type mockModel struct{ opts ai.Options }
func newMock(opts ...ai.Option) ai.Model {
m := &mockModel{}
_ = m.Init(opts...)
return m
}
func (m *mockModel) Init(opts ...ai.Option) error {
for _, o := range opts {
o(&m.opts)
}
return nil
}
func (m *mockModel) Options() ai.Options { return m.opts }
func (m *mockModel) String() string { return "first-agent-mock" }
func (m *mockModel) Stream(context.Context, *ai.Request, ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("stream not supported by first-agent mock")
}
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
for _, tool := range req.Tools {
if strings.Contains(tool.Name, "List") && m.opts.ToolHandler != nil {
m.opts.ToolHandler(ctx, ai.ToolCall{ID: "list-notes", Name: tool.Name, Input: map[string]any{}})
break
}
}
return &ai.Response{Answer: "Your first agent read the notes service and found three steps: install the CLI, run a service, then chat with an agent."}, nil
}
func waitFor(reg registry.Registry, names ...string) error {
deadline := time.Now().Add(5 * time.Second)
for _, name := range names {
for {
if svcs, err := reg.GetService(name); err == nil && len(svcs) > 0 && len(svcs[0].Nodes) > 0 {
break
}
if time.Now().After(deadline) {
return fmt.Errorf("timed out waiting for %s", name)
}
time.Sleep(20 * time.Millisecond)
}
}
return nil
}
func runFirstAgent() error {
ai.Register("first-agent-mock", newMock)
reg := registry.NewMemoryRegistry()
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))))
notes := service.New(service.Name("notes"), service.Address("127.0.0.1:0"), service.Registry(reg), service.Client(cl), service.HandleSignal(false))
if err := notes.Handle(new(NotesService)); err != nil {
return fmt.Errorf("handle notes: %w", err)
}
svcErr := make(chan error, 1)
go func() { svcErr <- notes.Run() }()
defer notes.Server().Stop()
assistant := agent.New(
agent.Name("assistant"),
agent.Address("127.0.0.1:0"),
agent.Services("notes"),
agent.Prompt("You are a friendly first agent. Use the notes service before answering."),
agent.Provider("first-agent-mock"),
agent.WithRegistry(reg),
agent.WithClient(cl),
agent.WithStore(store.NewMemoryStore()),
)
agentErr := make(chan error, 1)
go func() { agentErr <- assistant.Run() }()
defer assistant.Stop()
if err := waitFor(reg, "notes", "assistant"); err != nil {
select {
case runErr := <-svcErr:
return fmt.Errorf("run notes: %w", runErr)
case runErr := <-agentErr:
return fmt.Errorf("run assistant: %w", runErr)
default:
}
return err
}
fmt.Println("First agent (provider: mock, no API key)")
fmt.Println("> Summarize my next steps")
resp, err := assistant.Ask(context.Background(), "Summarize my next steps")
if err != nil {
return fmt.Errorf("ask assistant: %w", err)
}
fmt.Println("assistant:", resp.Reply)
fmt.Println("✓ service-backed agent completed without provider secrets")
return nil
}
func main() {
if err := runFirstAgent(); err != nil {
fmt.Println(err)
os.Exit(1)
}
}
-9
View File
@@ -1,9 +0,0 @@
package main
import "testing"
func TestRunFirstAgent(t *testing.T) {
if err := runFirstAgent(); err != nil {
t.Fatalf("first-agent example failed: %v", err)
}
}
+1 -3
View File
@@ -131,8 +131,6 @@ const delegatedNotifyTask = "Use the notify Send tool exactly once to tell owner
const commsPrompt = "You handle outbound notifications. When asked to notify someone, you must call the notify Send tool exactly once before replying. Never claim a notification was sent unless the notify tool returned success."
const delegatedNotifySettleTimeout = 10 * time.Second
type SendRequest struct {
To string `json:"to" description:"Recipient address"`
Message string `json:"message" description:"Message body"`
@@ -466,7 +464,7 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
if recoverMissingNotify == nil || tasks != 3 || notify != 0 {
return fmt.Errorf("delegation completed without required notify side effect: notify=%d, want 1", notify)
}
settled, err := waitForNotifySideEffect(notifySvc, delegatedNotifySettleTimeout)
settled, err := waitForNotifySideEffect(notifySvc, 2*time.Second)
if err != nil {
return err
}
+4 -5
View File
@@ -90,11 +90,10 @@ The console discovers services from the registry and orchestrates across them vi
After this quick start, follow the agent path in order:
1. [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.
2. [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.
3. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
4. [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.
5. [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`.
1. [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.
2. [Your First Agent](guides/your-first-agent.html) — build a service-backed agent and talk to it with `micro chat`.
3. [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.
4. [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`.
## Quick Start: Write a Service