项目文件夹

文件
2026-07-03 16:25:49 +01:00

498 行
16 KiB
Go

// Plan & Delegate integration harness.
//
// This runs the REAL go-micro stack end to end — real services, real
// registry, real RPC, the real agent loop, real store, real delegate
// routing — and mocks ONLY the LLM with a deterministic provider. It
// proves the plumbing works without an API key, and it's reproducible.
//
// Swap MICRO_AI_PROVIDER/MICRO_AI_API_KEY (and remove --mock) to run the
// exact same flow against a live model.
//
// Run:
//
// go run ./internal/harness/plan-delegate
package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"os"
"strings"
"sync"
"time"
"go-micro.dev/v6/agent"
"go-micro.dev/v6/ai"
"go-micro.dev/v6/broker"
"go-micro.dev/v6/flow"
"go-micro.dev/v6/internal/harness/harnessutil"
"go-micro.dev/v6/registry"
"go-micro.dev/v6/service"
"go-micro.dev/v6/store"
)
// ---------------------------------------------------------------------------
// real services
// ---------------------------------------------------------------------------
type Task struct {
ID string `json:"id"`
Title string `json:"title"`
}
type AddRequest struct {
Title string `json:"title" description:"Title of the task to add"`
}
type AddResponse struct {
Task *Task `json:"task"`
}
type ListRequest struct{}
type ListResponse struct {
Tasks []*Task `json:"tasks"`
}
type TaskService struct {
mu sync.Mutex
tasks []*Task
byTitle map[string]*Task
nextID int
}
// Add creates a new task with the given title. Replayed live-model tool calls
// are idempotent by launch task title so the conformance harness proves exactly
// one durable side effect per intended task even if a provider resends a call.
// @example {"title": "Design"}
func (s *TaskService) Add(ctx context.Context, req *AddRequest, rsp *AddResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
if s.byTitle == nil {
s.byTitle = map[string]*Task{}
}
key := launchTaskKey(req.Title)
if t := s.byTitle[key]; t != nil {
rsp.Task = t
fmt.Printf(" \033[32m[task]\033[0m reused %s %q\n", t.ID, t.Title)
return nil
}
s.nextID++
t := &Task{ID: fmt.Sprintf("task-%d", s.nextID), Title: canonicalLaunchTitle(req.Title)}
s.tasks = append(s.tasks, t)
s.byTitle[key] = t
rsp.Task = t
fmt.Printf(" \033[32m[task]\033[0m created %s %q\n", t.ID, t.Title)
return nil
}
func launchTaskKey(title string) string {
s := strings.ToLower(strings.TrimSpace(title))
switch {
case strings.Contains(s, "design"):
return "design"
case strings.Contains(s, "build"):
return "build"
case strings.Contains(s, "ship"):
return "ship"
default:
return s
}
}
func canonicalLaunchTitle(title string) string {
switch launchTaskKey(title) {
case "design":
return "Design"
case "build":
return "Build"
case "ship":
return "Ship"
default:
return strings.TrimSpace(title)
}
}
// List returns all tasks.
// @example {}
func (s *TaskService) List(ctx context.Context, req *ListRequest, rsp *ListResponse) error {
s.mu.Lock()
defer s.mu.Unlock()
rsp.Tasks = append(rsp.Tasks, s.tasks...)
return nil
}
func (s *TaskService) count() int {
s.mu.Lock()
defer s.mu.Unlock()
return len(s.tasks)
}
const delegatedNotifyTask = "Use the notify Send tool exactly once to tell owner@acme.com: The launch plan is ready. Do not answer until the notify tool call has succeeded."
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."
type SendRequest struct {
To string `json:"to" description:"Recipient address"`
Message string `json:"message" description:"Message body"`
}
type SendResponse struct {
Sent bool `json:"sent"`
}
type NotifyService struct {
mu sync.Mutex
sent int
attempts int
duplicates int
bySend map[string]bool
}
// Send delivers a notification message to a recipient. Duplicate delivery
// attempts for the same recipient/message are treated as successful replays
// without producing another side effect.
// @example {"to": "owner@acme.com", "message": "ready"}
func (s *NotifyService) Send(ctx context.Context, req *SendRequest, rsp *SendResponse) error {
s.mu.Lock()
if s.bySend == nil {
s.bySend = map[string]bool{}
}
key := strings.ToLower(strings.TrimSpace(req.To)) + "\x00" + strings.ToLower(strings.TrimSpace(req.Message))
s.attempts++
if !s.bySend[key] {
s.bySend[key] = true
s.sent++
fmt.Printf(" \033[35m[notify]\033[0m 📨 to=%s message=%q\n", req.To, req.Message)
} else {
s.duplicates++
fmt.Printf(" \033[35m[notify]\033[0m reused to=%s message=%q\n", req.To, req.Message)
}
s.mu.Unlock()
rsp.Sent = true
return nil
}
func (s *NotifyService) count() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.sent
}
func (s *NotifyService) duplicateAttempts() int {
s.mu.Lock()
defer s.mu.Unlock()
return s.duplicates
}
// ---------------------------------------------------------------------------
// mock LLM provider — the ONLY fake. It "reasons" by simple heuristics
// over the tools it's offered and the system prompt it's given, calling
// the real tool handler exactly the way a real provider would.
// ---------------------------------------------------------------------------
type mockModel struct {
opts ai.Options
// unknownDelegateOnce makes the mock emit one provider-style, unavailable
// delegate tool name before using the registered delegate tool. This mirrors
// live providers that occasionally hallucinate a provider-specific tool while
// still keeping the regression deterministic and keyless.
unknownDelegateOnce bool
emittedUnknownDelegate bool
}
func newMock(opts ...ai.Option) ai.Model {
m := &mockModel{}
_ = m.Init(opts...)
return m
}
func newMockUnknownDelegate(opts ...ai.Option) ai.Model {
m := &mockModel{unknownDelegateOnce: true}
_ = 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 "mock" }
func (m *mockModel) Stream(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (ai.Stream, error) {
return nil, fmt.Errorf("stream not supported by mock")
}
// findTool returns the safe name of the first offered tool whose name
// contains sub, or "" if none.
func findTool(tools []ai.Tool, sub string) string {
for _, t := range tools {
if strings.Contains(t.Name, sub) {
return t.Name
}
}
return ""
}
func (m *mockModel) call(who, name string, input map[string]any) {
args, _ := json.Marshal(input)
fmt.Printf(" \033[33m[%s]\033[0m → %s(%s)\n", who, name, args)
if m.opts.ToolHandler != nil {
m.opts.ToolHandler(context.Background(), ai.ToolCall{Name: name, Input: input})
}
}
func (m *mockModel) Generate(ctx context.Context, req *ai.Request, _ ...ai.GenerateOption) (*ai.Response, error) {
// Classify by the tools actually offered, not by prompt text:
// the conductor has the task "Add" tool, comms has "Send".
hasAdd := findTool(req.Tools, "Add") != ""
hasSend := findTool(req.Tools, "Send") != ""
switch {
// comms agent: owns notify, has Send but not Add.
case hasSend && !hasAdd:
send := findTool(req.Tools, "Send")
m.call("comms", send, map[string]any{
"to": "owner@acme.com",
"message": "The launch plan is ready",
})
return &ai.Response{Answer: "Notified owner@acme.com."}, nil
// conductor: has the task Add tool — plan, create tasks, delegate.
case hasAdd:
if plan := findTool(req.Tools, "plan"); plan != "" {
m.call("conductor", plan, map[string]any{
"steps": []any{
map[string]any{"task": "create Design task", "status": "pending"},
map[string]any{"task": "create Build task", "status": "pending"},
map[string]any{"task": "create Ship task", "status": "pending"},
map[string]any{"task": "notify owner via comms", "status": "pending"},
},
})
}
if add := findTool(req.Tools, "Add"); add != "" {
for _, title := range []string{"Design", "Build", "Ship"} {
m.call("conductor", add, map[string]any{"title": title})
}
}
if del := findTool(req.Tools, "delegate"); del != "" {
if m.unknownDelegateOnce && !m.emittedUnknownDelegate {
m.emittedUnknownDelegate = true
m.call("conductor", "atlascloud_delegate", map[string]any{
"task": delegatedNotifyTask,
"to": "comms",
})
} else {
m.call("conductor", del, map[string]any{
"task": delegatedNotifyTask,
"to": "comms",
})
}
}
return &ai.Response{Answer: "Created Design, Build and Ship, and had comms notify the owner."}, nil
// ephemeral sub-agent or anything else.
default:
return &ai.Response{Reply: "subtask handled"}, nil
}
}
func providerKey(provider string) string {
if v := os.Getenv("MICRO_AI_API_KEY"); v != "" {
return v
}
env := map[string]string{
"anthropic": "ANTHROPIC_API_KEY",
"openai": "OPENAI_API_KEY",
"gemini": "GEMINI_API_KEY",
"groq": "GROQ_API_KEY",
"mistral": "MISTRAL_API_KEY",
"together": "TOGETHER_API_KEY",
"atlascloud": "ATLASCLOUD_API_KEY",
}[provider]
return os.Getenv(env)
}
func runPlanDelegate(provider string) error {
apiKey := ""
switch provider {
case "mock":
ai.Register("mock", newMock)
case "mock-unknown-delegate":
ai.Register("mock-unknown-delegate", newMockUnknownDelegate)
default:
apiKey = providerKey(provider)
if apiKey == "" {
fmt.Printf("no API key for provider %q — set MICRO_AI_API_KEY or the provider's key env\n", provider)
return nil
}
}
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 := harnessutil.Client(provider, reg)
mem := store.NewMemoryStore()
liveAgentOpts := harnessutil.AgentOptions(provider)
commsCheckpoint := flow.StoreCheckpoint(mem, "agent-comms")
conductorCheckpoint := flow.StoreCheckpoint(mem, "agent-conductor")
// 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 {
return fmt.Errorf("task handle: %w", err)
}
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 {
return fmt.Errorf("notify handle: %w", err)
}
go notify.Run()
// Real comms agent (owns notify), registered so delegate reaches it over RPC.
commsOpts := []agent.Option{
agent.Name("comms"),
agent.Address("127.0.0.1:0"),
agent.Services("notify"),
agent.Prompt(commsPrompt),
agent.Provider(provider), agent.APIKey(apiKey),
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
agent.WithCheckpoint(commsCheckpoint),
}
commsOpts = append(commsOpts, liveAgentOpts...)
comms := agent.New(commsOpts...)
go comms.Run()
defer comms.Stop()
// Real conductor agent (owns task), registered so the flow can reach it over RPC.
conductorOpts := []agent.Option{
agent.Name("conductor"),
agent.Address("127.0.0.1:0"),
agent.Services("task"),
agent.Prompt("You coordinate launch work. Plan first, create exactly one Design task, one Build task, and one Ship task, then delegate exactly one readiness notification to the \"comms\" agent. Do not create duplicate tasks and do not send notifications yourself."),
agent.Provider(provider), agent.APIKey(apiKey),
agent.WithRegistry(reg), agent.WithClient(cl), agent.WithStore(mem),
agent.WithCheckpoint(conductorCheckpoint),
}
conductorOpts = append(conductorOpts, liveAgentOpts...)
conductor := agent.New(conductorOpts...)
go conductor.Run()
defer conductor.Stop()
fmt.Println("waiting for services + agents to register...")
waitForService := func(name string) error {
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 nil
}
time.Sleep(20 * time.Millisecond)
}
return fmt.Errorf("service %q never registered", name)
}
for _, name := range []string{"task", "notify", "comms", "conductor"} {
if err := waitForService(name); err != nil {
return err
}
}
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}}"),
flow.Timeout(harnessutil.LiveTimeout(provider)),
)
if err := f.Register(reg, broker.DefaultBroker, cl); err != nil {
return fmt.Errorf("flow register: %w", err)
}
fmt.Print("\n\033[1m> flow:\033[0m services + agents + workflow + plan/delegate, no API key.\n\n")
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
executeDone := make(chan error, 1)
go func() {
executeDone <- f.Execute(ctx, "launch readiness")
}()
if err := waitForPlanDelegateExecution(executeDone, taskSvc, notifySvc, func(ctx context.Context) error {
_, err := conductor.Ask(ctx, "The Design, Build, and Ship tasks already exist, but the owner notification is still missing. Delegate exactly one notification to the \"comms\" agent now with this exact subtask: "+delegatedNotifyTask+" Do not create more tasks and do not answer until comms has handled the notification.")
return err
}); err != nil {
return err
}
if rs := f.Results(); len(rs) > 0 {
fmt.Println("\n\033[1m< conductor reply:\033[0m", rs[len(rs)-1].Reply)
}
// Prove plan was persisted to the real store.
if recs, _ := store.Scope(mem, "agent", "conductor").Read("plan"); len(recs) > 0 {
fmt.Printf("\n\033[1mstored plan (agent/conductor/plan):\033[0m %s\n", string(recs[0].Value))
} else {
return fmt.Errorf("plan was not persisted")
}
if taskSvc.count() != 3 || notifySvc.count() != 1 {
return fmt.Errorf("unexpected side effects: tasks=%d notify=%d", taskSvc.count(), notifySvc.count())
}
fmt.Println("\n\033[32m✓ 0→hero flow complete (services → agents → workflow)\033[0m")
return nil
}
func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notifySvc *NotifyService, recoverMissingNotify func(context.Context) error) error {
ticker := time.NewTicker(50 * time.Millisecond)
defer ticker.Stop()
for {
select {
case err := <-done:
tasks := taskSvc.count()
notify := notifySvc.count()
if err != nil {
if isClientTimeout(err) && tasks == 3 && notify == 1 {
fmt.Printf("\n\033[33mwarning:\033[0m flow execute returned after completed side effects: %v\n", err)
return nil
}
return fmt.Errorf("flow execute after side effects tasks=%d notify=%d: %w", tasks, notify, err)
}
if notify != 1 {
if recoverMissingNotify == nil || tasks != 3 || notify != 0 {
return fmt.Errorf("delegation completed without required notify side effect: notify=%d, want 1", notify)
}
fmt.Print("\n\033[33mwarning:\033[0m flow completed before delegated notify; retrying the missing comms handoff once.\n")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
retryErr := recoverMissingNotify(ctx)
cancel()
if retryErr != nil {
return fmt.Errorf("delegation completed without required notify side effect and recovery failed: notify=%d, want 1: %w", notify, retryErr)
}
if notify = notifySvc.count(); notify != 1 {
return fmt.Errorf("delegation recovery completed without required notify side effect: notify=%d, want 1", notify)
}
}
return nil
case <-ticker.C:
if dup := notifySvc.duplicateAttempts(); dup > 0 {
return fmt.Errorf("duplicate notify attempts: got %d duplicate replay(s), want 0", dup)
}
}
}
}
func isClientTimeout(err error) bool {
msg := strings.ToLower(err.Error())
return strings.Contains(msg, "request timeout") || strings.Contains(msg, "code=408") || strings.Contains(msg, "code\":408")
}
func main() {
provider := flag.String("provider", "mock", "LLM provider: mock (default), mock-unknown-delegate, anthropic, openai, gemini, groq, mistral, together, atlascloud")
flag.Parse()
if err := runPlanDelegate(*provider); err != nil {
fmt.Println("\033[31merror:\033[0m", err)
os.Exit(1)
}
}