Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d3cc29a0f7 |
@@ -21,9 +21,10 @@ changes, architectural rewrites. Those go to the human.
|
||||
|
||||
## Work queue (ranked)
|
||||
|
||||
1. **Make AtlasCloud live conformance exercise the guarded delegate path** ([#3990](https://github.com/micro/go-micro/issues/3990)) — #3993 shipped the highest-priority adoption wayfinding gap by surfacing first-agent and 0→hero next steps from `micro new`, so the queue should not re-run #3983. The current top risk is a live provider-conformance regression in the Now-phase “battle-tested across providers” contract: AtlasCloud completed the model/tool path but skipped the guarded delegation attempt, leaving the harness unable to prove delegation refusal behavior across providers.
|
||||
2. **Make AtlasCloud plan/delegate complete required tasks before notification** ([#3991](https://github.com/micro/go-micro/issues/3991)) — the same scheduled live harness exposed a 0→hero correctness seam: the conductor emitted the delegated notification side effect while required task-creation plan steps remained unfinished. This is ranked just behind #3990 because it protects the services → agents → workflows story users now discover through the CLI and should fail safely before side effects or recover before checkpoint validation.
|
||||
3. **Broaden provider streaming and keep chat/A2A streaming end to end** ([#3903](https://github.com/micro/go-micro/issues/3903)) — once the live conformance regressions are fixed, streaming remains 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. **Mark AtlasCloud delegated plan steps complete** ([#3946](https://github.com/micro/go-micro/issues/3946)) — the latest live Harness (E2E) run got farther than the earlier task-call blocker: AtlasCloud/minimax-m3 created all three tasks and sent the delegated notification (`tasks=3 notify=1`), but the persisted conductor plan still left the delegation step unfinished. This is the top Now-phase item because it is the active red master conformance gate and directly tests whether services → agents → workflows state stays coherent after real delegation side effects.
|
||||
2. **Stabilize AtlasCloud guarded delegate conformance** ([#3917](https://github.com/micro/go-micro/issues/3917)) — #3948 just merged stronger AtlasCloud delegate prompting and tagged fallback coverage, so this may be resolved by the next live run, but the issue is still open and has recurred multiple times. Keep it high until master's provider harness proves the guarded delegate reaches the approval-refusal path deterministically, then drop it immediately rather than re-queueing done conformance work.
|
||||
3. **Execute AtlasCloud plan-delegate task tool calls** ([#3935](https://github.com/micro/go-micro/issues/3935)) — #3942 appears to have moved the live plan-delegate path past the tagged task-call failure, but the issue remains open. Leave it below the newer #3946 completion-state failure and close/drop it once the maintainers confirm the task-call side is fixed by the same live harness evidence.
|
||||
4. **Broaden provider streaming and keep chat/A2A streaming end to end** ([#3903](https://github.com/micro/go-micro/issues/3903)) — once the live Now-phase conformance gates are green, 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.
|
||||
5. **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.
|
||||
_Seeded by Claude Code from the roadmap + open issues; thereafter maintained by the
|
||||
architecture-review pass._
|
||||
|
||||
@@ -440,12 +440,9 @@ resp, _ := m.Generate(ctx, &ai.Request{Prompt: "hello"})
|
||||
|
||||
## Examples
|
||||
|
||||
New to agents? Follow the [first-agent on-ramp](#first-agent-on-ramp), then use the [examples index](examples/README.md) for the full services → agents → workflows map.
|
||||
|
||||
- [hello-world](examples/hello-world/) — Basic RPC service
|
||||
- [multi-service](examples/multi-service/) — Multiple services in one binary
|
||||
- [mcp](examples/mcp/) — MCP integration with AI agents
|
||||
- [first-agent](examples/first-agent/) — Smallest provider-free service-backed agent
|
||||
- [agent-plan-delegate](examples/agent-plan-delegate/) — Agent planning and multi-agent delegation
|
||||
- [agent-durable](examples/agent-durable/) — Checkpoint and resume an agent run without replaying completed tool side effects
|
||||
- [grpc-interop](examples/grpc-interop/) — Call go-micro from any gRPC client
|
||||
|
||||
+2
-6
@@ -460,7 +460,7 @@ func (a *agentImpl) Run() error {
|
||||
a.setup()
|
||||
}
|
||||
|
||||
serverOpts := []server.Option{
|
||||
a.server = server.NewServer(
|
||||
server.Name(a.opts.Name),
|
||||
server.Address(a.opts.Address),
|
||||
server.Registry(a.opts.Registry),
|
||||
@@ -468,11 +468,7 @@ func (a *agentImpl) Run() error {
|
||||
"type": "agent",
|
||||
"services": strings.Join(a.opts.Services, ","),
|
||||
}),
|
||||
}
|
||||
if a.opts.Broker != nil {
|
||||
serverOpts = append(serverOpts, server.Broker(a.opts.Broker))
|
||||
}
|
||||
a.server = server.NewServer(serverOpts...)
|
||||
)
|
||||
|
||||
_ = pb.RegisterAgentHandler(a.server, a)
|
||||
|
||||
|
||||
@@ -191,14 +191,13 @@ func askWithConformanceRetry(ctx context.Context, a Agent, initialPrompt string,
|
||||
}
|
||||
sawRequiredTool := sawTool == nil || *sawTool
|
||||
sawRequiredDelegate := sawBlockedDelegate == nil || *sawBlockedDelegate
|
||||
hasMarker := responseHasConformanceMarker(resp)
|
||||
if sawRequiredTool && sawRequiredDelegate && hasMarker {
|
||||
if sawRequiredTool && sawRequiredDelegate {
|
||||
return resp, nil
|
||||
}
|
||||
if attempt == maxAttempts {
|
||||
break
|
||||
}
|
||||
prompt = nextConformanceRetryPrompt(sawRequiredTool, sawRequiredDelegate, hasMarker)
|
||||
prompt = nextConformanceRetryPrompt(sawRequiredTool, sawRequiredDelegate)
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
@@ -232,26 +231,17 @@ func TestAgentProviderConformanceAtlasCloudPromptRequiresTaggedDelegateFallback(
|
||||
}
|
||||
}
|
||||
|
||||
func nextConformanceRetryPrompt(sawTool, sawBlockedDelegate, hasMarker bool) string {
|
||||
func nextConformanceRetryPrompt(sawTool, sawBlockedDelegate bool) string {
|
||||
switch {
|
||||
case !sawTool:
|
||||
return "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."
|
||||
case !sawBlockedDelegate:
|
||||
return "The previous response called conformance_echo but did not attempt the required guarded delegation. Continue the same conformance check now: call delegate exactly once with input {\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}; do not answer in prose until that delegate call has been attempted. If native tool_calls are unavailable, emit exactly <tool_call name=\"delegate\">{\"task\":\"summarize the conformance marker\",\"to\":\"blocked-reviewer\"}</tool_call>. The delegate is expected to be refused by policy; include that refusal and the agent-conformance marker in the final answer."
|
||||
case !hasMarker:
|
||||
return "The previous response completed the required tool calls but omitted the conformance marker. Continue the same conformance check now: do not call more tools; answer with the prior echo result marker agent-conformance-ok and mention the guarded delegate refusal."
|
||||
default:
|
||||
return "Retry the provider conformance check and include the agent-conformance marker in the final answer."
|
||||
}
|
||||
}
|
||||
|
||||
func responseHasConformanceMarker(resp *Response) bool {
|
||||
if resp == nil {
|
||||
return false
|
||||
}
|
||||
return strings.Contains(resp.Reply, "agent-conformance-ok") || strings.Contains(resp.Reply, "agent-conformance")
|
||||
}
|
||||
|
||||
func validateConformanceRequest(req *ai.Request, opts ai.Options) error {
|
||||
if req.Prompt == "" {
|
||||
return errors.New("missing prompt")
|
||||
@@ -342,47 +332,6 @@ func TestAgentProviderConformanceRetriesMissingTool(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceRetriesMissingMarker(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: "called conformance_echo and handled guarded delegate refusal without the required marker"}, nil
|
||||
}
|
||||
return &ai.Response{Reply: "agent-conformance-ok after guarded delegate refusal"}, nil
|
||||
}
|
||||
defer func() { fakeGen = nil }()
|
||||
|
||||
sawTool := true
|
||||
sawBlockedDelegate := true
|
||||
a := New(
|
||||
Name("conformance-retry-marker"),
|
||||
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) {
|
||||
return `{"marker":"agent-conformance-ok"}`, nil
|
||||
}),
|
||||
)
|
||||
|
||||
resp, err := askWithConformanceRetry(context.Background(), a, "Run the provider conformance check.", &sawTool, &sawBlockedDelegate)
|
||||
if err != nil {
|
||||
t.Fatalf("Ask: %v", err)
|
||||
}
|
||||
if attempts != 2 {
|
||||
t.Fatalf("attempts = %d, want retry after missing marker", attempts)
|
||||
}
|
||||
if !strings.Contains(resp.Reply, "agent-conformance-ok") {
|
||||
t.Fatalf("Reply = %q, want conformance marker", resp.Reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgentProviderConformanceRetriesMissingDelegate(t *testing.T) {
|
||||
var attempts int
|
||||
fakeGen = func(ctx context.Context, opts ai.Options, req *ai.Request) (*ai.Response, error) {
|
||||
|
||||
@@ -5,7 +5,6 @@ import (
|
||||
"time"
|
||||
|
||||
"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"
|
||||
@@ -45,7 +44,6 @@ type Options struct {
|
||||
Address string
|
||||
Registry registry.Registry
|
||||
Client client.Client
|
||||
Broker broker.Broker
|
||||
Store store.Store
|
||||
HistoryLimit int
|
||||
|
||||
@@ -194,13 +192,6 @@ func WithClient(c client.Client) Option {
|
||||
return func(o *Options) { o.Client = c }
|
||||
}
|
||||
|
||||
// WithBroker sets the broker used by the agent service endpoint. Use an
|
||||
// in-memory broker in local harnesses/tests to avoid sharing the package-wide
|
||||
// default broker listener across concurrently running examples.
|
||||
func WithBroker(b broker.Broker) Option {
|
||||
return func(o *Options) { o.Broker = b }
|
||||
}
|
||||
|
||||
// WithStore sets the store for agent memory.
|
||||
func WithStore(s store.Store) Option {
|
||||
return func(o *Options) { o.Store = s }
|
||||
|
||||
+14
-35
@@ -48,7 +48,13 @@ func (a *agentImpl) executeTextToolCalls(ctx context.Context, reply string, tool
|
||||
}
|
||||
|
||||
func parseTextToolCalls(text string, tools []ai.Tool) []ai.ToolCall {
|
||||
allowed := textToolNames(tools)
|
||||
allowed := map[string]bool{}
|
||||
for _, tool := range tools {
|
||||
allowed[tool.Name] = true
|
||||
if tool.OriginalName != "" {
|
||||
allowed[tool.OriginalName] = true
|
||||
}
|
||||
}
|
||||
if len(allowed) == 0 {
|
||||
return nil
|
||||
}
|
||||
@@ -64,33 +70,6 @@ func parseTextToolCalls(text string, tools []ai.Tool) []ai.ToolCall {
|
||||
return nil
|
||||
}
|
||||
|
||||
func textToolNames(tools []ai.Tool) map[string]string {
|
||||
allowed := map[string]string{}
|
||||
for _, tool := range tools {
|
||||
addTextToolName(allowed, tool.Name, tool.Name)
|
||||
if tool.OriginalName != "" {
|
||||
addTextToolName(allowed, tool.OriginalName, tool.Name)
|
||||
}
|
||||
}
|
||||
return allowed
|
||||
}
|
||||
|
||||
func addTextToolName(allowed map[string]string, name, canonical string) {
|
||||
if name == "" || canonical == "" {
|
||||
return
|
||||
}
|
||||
allowed[name] = canonical
|
||||
// Some OpenAI-compatible models describe an idempotent Add endpoint as a
|
||||
// creation action and emit the otherwise-correct service tool with a Create
|
||||
// suffix in text-only tool-call markup. Keep the fallback bounded by the
|
||||
// offered service tool prefix so ordinary unknown tools remain ignored.
|
||||
for _, suffix := range []string{"_Add", ".Add"} {
|
||||
if strings.HasSuffix(name, suffix) {
|
||||
allowed[strings.TrimSuffix(name, suffix)+strings.Replace(suffix, "Add", "Create", 1)] = canonical
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func jsonCandidates(text string) []string {
|
||||
trimmed := strings.TrimSpace(text)
|
||||
var out []string
|
||||
@@ -113,7 +92,7 @@ func jsonCandidates(text string) []string {
|
||||
return out
|
||||
}
|
||||
|
||||
func decodeTextToolCalls(candidate string, allowed map[string]string) []ai.ToolCall {
|
||||
func decodeTextToolCalls(candidate string, allowed map[string]bool) []ai.ToolCall {
|
||||
var root any
|
||||
if err := json.Unmarshal([]byte(candidate), &root); err != nil {
|
||||
return nil
|
||||
@@ -121,7 +100,7 @@ func decodeTextToolCalls(candidate string, allowed map[string]string) []ai.ToolC
|
||||
return collectTextToolCalls(root, allowed)
|
||||
}
|
||||
|
||||
func collectTextToolCalls(v any, allowed map[string]string) []ai.ToolCall {
|
||||
func collectTextToolCalls(v any, allowed map[string]bool) []ai.ToolCall {
|
||||
switch x := v.(type) {
|
||||
case []any:
|
||||
var out []ai.ToolCall
|
||||
@@ -142,20 +121,20 @@ func collectTextToolCalls(v any, allowed map[string]string) []ai.ToolCall {
|
||||
if input == nil {
|
||||
input = call.Arguments
|
||||
}
|
||||
if name == "" || allowed[name] == "" || input == nil {
|
||||
if name == "" || !allowed[name] || input == nil {
|
||||
return nil
|
||||
}
|
||||
id := call.ID
|
||||
if id == "" {
|
||||
id = fmt.Sprintf("text-call-%s", strings.ReplaceAll(name, ".", "_"))
|
||||
}
|
||||
return []ai.ToolCall{{ID: id, Name: allowed[name], Input: input}}
|
||||
return []ai.ToolCall{{ID: id, Name: name, Input: input}}
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
func decodeTaggedTextToolCalls(text string, allowed map[string]string) []ai.ToolCall {
|
||||
func decodeTaggedTextToolCalls(text string, allowed map[string]bool) []ai.ToolCall {
|
||||
var out []ai.ToolCall
|
||||
for _, match := range singleTaggedToolCall.FindAllStringSubmatch(text, -1) {
|
||||
if len(match) < 3 {
|
||||
@@ -171,7 +150,7 @@ func decodeTaggedTextToolCalls(text string, allowed map[string]string) []ai.Tool
|
||||
continue
|
||||
}
|
||||
name := taggedToolName(tag)
|
||||
if name == "" || allowed[name] == "" {
|
||||
if name == "" || !allowed[name] {
|
||||
continue
|
||||
}
|
||||
var input map[string]any
|
||||
@@ -180,7 +159,7 @@ func decodeTaggedTextToolCalls(text string, allowed map[string]string) []ai.Tool
|
||||
}
|
||||
out = append(out, ai.ToolCall{
|
||||
ID: fmt.Sprintf("text-call-%s", strings.ReplaceAll(name, ".", "_")),
|
||||
Name: allowed[name],
|
||||
Name: name,
|
||||
Input: input,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -40,19 +40,3 @@ func TestParseTextToolCallsFunctionTaggedMarkup(t *testing.T) {
|
||||
t.Fatalf("title = %v, want Design", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseTextToolCallsCreateAliasForAddTool(t *testing.T) {
|
||||
tools := []ai.Tool{{Name: "task_TaskService_Add", OriginalName: "task.TaskService.Add"}}
|
||||
reply := `<tool_call>{"name":"task_TaskService_Create","arguments":{"title":"Design"}}</tool_call>`
|
||||
|
||||
calls := parseTextToolCalls(reply, tools)
|
||||
if len(calls) != 1 {
|
||||
t.Fatalf("parseTextToolCalls returned %d calls, want 1: %+v", len(calls), calls)
|
||||
}
|
||||
if calls[0].Name != "task_TaskService_Add" {
|
||||
t.Fatalf("call name = %q, want canonical task_TaskService_Add", calls[0].Name)
|
||||
}
|
||||
if got := calls[0].Input["title"]; got != "Design" {
|
||||
t.Fatalf("title = %v, want Design", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
package new
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"flag"
|
||||
"os"
|
||||
@@ -58,43 +57,6 @@ func TestZeroToOneNoMCPContract(t *testing.T) {
|
||||
generated.call(t, "Bob", "Hello Bob")
|
||||
}
|
||||
|
||||
func TestPrintNextStepsSurfacesFirstAgentPath(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
printNextSteps(&out, "helloworld", false)
|
||||
|
||||
for _, want := range []string{
|
||||
"cd helloworld",
|
||||
"micro agent preflight",
|
||||
"go run .",
|
||||
"micro chat",
|
||||
"micro inspect agent",
|
||||
"micro docs",
|
||||
"your-first-agent.html",
|
||||
"zero-to-hero.html",
|
||||
"http://localhost:3001/mcp/tools",
|
||||
} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("next steps missing %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPrintNextStepsNoMCPSkipsMCPHints(t *testing.T) {
|
||||
var out bytes.Buffer
|
||||
printNextSteps(&out, "worker", true)
|
||||
|
||||
for _, want := range []string{"micro agent preflight", "micro chat", "micro inspect agent", "micro docs"} {
|
||||
if !strings.Contains(out.String(), want) {
|
||||
t.Fatalf("--no-mcp next steps missing %q:\n%s", want, out.String())
|
||||
}
|
||||
}
|
||||
for _, notWant := range []string{"http://localhost:3001/mcp/tools", "micro mcp serve"} {
|
||||
if strings.Contains(out.String(), notWant) {
|
||||
t.Fatalf("--no-mcp next steps should not include %q:\n%s", notWant, out.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type generatedService struct {
|
||||
dir string
|
||||
repoRoot string
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"go/build"
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
@@ -281,28 +280,16 @@ func Run(ctx *cli.Context) error {
|
||||
|
||||
fmt.Println()
|
||||
fmt.Printf(" \033[32m✓\033[0m Service \033[36m%s\033[0m created\n\n", dir)
|
||||
printNextSteps(os.Stdout, dir, noMCP)
|
||||
return nil
|
||||
}
|
||||
|
||||
func printNextSteps(w io.Writer, dir string, noMCP bool) {
|
||||
fmt.Fprintln(w, " Next steps:")
|
||||
fmt.Fprintf(w, " cd %s\n", dir)
|
||||
fmt.Fprintln(w, " micro agent preflight")
|
||||
fmt.Fprintln(w, " go run .")
|
||||
fmt.Fprintln(w, " micro chat")
|
||||
fmt.Fprintln(w, " micro inspect agent")
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintln(w, " First-agent path:")
|
||||
fmt.Fprintln(w, " micro docs")
|
||||
fmt.Fprintln(w, " https://go-micro.dev/docs/guides/your-first-agent.html")
|
||||
fmt.Fprintln(w, " https://go-micro.dev/docs/guides/zero-to-hero.html")
|
||||
fmt.Println(" Next steps:")
|
||||
fmt.Printf(" cd %s\n", dir)
|
||||
fmt.Println(" go run .")
|
||||
if !noMCP {
|
||||
fmt.Fprintln(w)
|
||||
fmt.Fprintf(w, " MCP tools \033[36mhttp://localhost:3001/mcp/tools\033[0m\n")
|
||||
fmt.Fprintln(w, " Claude Code \033[2mmicro mcp serve\033[0m")
|
||||
fmt.Println()
|
||||
fmt.Printf(" MCP tools \033[36mhttp://localhost:3001/mcp/tools\033[0m\n")
|
||||
fmt.Println(" Claude Code \033[2mmicro mcp serve\033[0m")
|
||||
}
|
||||
fmt.Fprintln(w)
|
||||
fmt.Println()
|
||||
return nil
|
||||
}
|
||||
|
||||
func selectTemplates(name string, noMCP bool) (mainTmpl, handlerTmpl, protoTmpl string) {
|
||||
|
||||
@@ -19,7 +19,6 @@ import (
|
||||
|
||||
"go-micro.dev/v6/agent"
|
||||
"go-micro.dev/v6/ai"
|
||||
"go-micro.dev/v6/broker"
|
||||
"go-micro.dev/v6/client"
|
||||
"go-micro.dev/v6/registry"
|
||||
"go-micro.dev/v6/selector"
|
||||
@@ -93,17 +92,9 @@ func runFirstAgent() error {
|
||||
ai.Register("first-agent-mock", newMock)
|
||||
|
||||
reg := registry.NewMemoryRegistry()
|
||||
br := broker.NewMemoryBroker()
|
||||
if err := br.Init(); err != nil {
|
||||
return fmt.Errorf("init broker: %w", err)
|
||||
}
|
||||
if err := br.Connect(); err != nil {
|
||||
return fmt.Errorf("connect broker: %w", err)
|
||||
}
|
||||
defer br.Disconnect()
|
||||
cl := client.NewClient(client.Registry(reg), client.Selector(selector.NewSelector(selector.Registry(reg))), client.Broker(br))
|
||||
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.Broker(br), service.HandleSignal(false))
|
||||
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)
|
||||
}
|
||||
@@ -119,7 +110,6 @@ func runFirstAgent() error {
|
||||
agent.Provider("first-agent-mock"),
|
||||
agent.WithRegistry(reg),
|
||||
agent.WithClient(cl),
|
||||
agent.WithBroker(br),
|
||||
agent.WithStore(store.NewMemoryStore()),
|
||||
)
|
||||
agentErr := make(chan error, 1)
|
||||
|
||||
@@ -403,14 +403,8 @@ func runPlanDelegate(provider string) error {
|
||||
}
|
||||
|
||||
f := flow.New("zero-to-hero",
|
||||
flow.Steps(
|
||||
flow.Step{Name: "conductor", Run: planDelegateConductorStep(conductor)},
|
||||
flow.Step{Name: "require-notify", Run: requireDelegatedNotifyStep(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
|
||||
})},
|
||||
),
|
||||
flow.WithCheckpoint(flow.StoreCheckpoint(mem, "flow-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 {
|
||||
@@ -425,8 +419,15 @@ func runPlanDelegate(provider string) error {
|
||||
executeDone <- f.Execute(ctx, "launch readiness")
|
||||
}()
|
||||
|
||||
if err := waitForPlanDelegateExecution(executeDone, taskSvc, notifySvc); err != nil {
|
||||
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.
|
||||
@@ -443,48 +444,7 @@ func runPlanDelegate(provider string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func planDelegateConductorStep(conductor agent.Agent) flow.StepFunc {
|
||||
return func(ctx context.Context, in flow.State) (flow.State, error) {
|
||||
prompt := "Create three launch tasks (Design, Build, Ship), then make sure owner@acme.com is notified: " + in.String()
|
||||
rsp, err := conductor.Ask(ctx, prompt)
|
||||
if err != nil {
|
||||
return in, err
|
||||
}
|
||||
if rsp != nil && rsp.Reply != "" {
|
||||
fmt.Println("\n\033[1m< conductor reply:\033[0m", rsp.Reply)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
|
||||
func requireDelegatedNotifyStep(taskSvc *TaskService, notifySvc *NotifyService, recoverMissingNotify func(context.Context) error) flow.StepFunc {
|
||||
return func(ctx context.Context, in flow.State) (flow.State, error) {
|
||||
tasks := taskSvc.count()
|
||||
notify := notifySvc.count()
|
||||
if notify == 1 {
|
||||
return in, nil
|
||||
}
|
||||
if recoverMissingNotify == nil || tasks != 3 || notify != 0 {
|
||||
return in, fmt.Errorf("delegation completed without required notify side effect: notify=%d, want 1", notify)
|
||||
}
|
||||
settled, err := waitForNotifySideEffect(notifySvc, delegatedNotifySettleTimeout)
|
||||
if err != nil {
|
||||
return in, err
|
||||
}
|
||||
if !settled {
|
||||
fmt.Print("\n\033[33mwarning:\033[0m conductor step completed before delegated notify; retrying the missing comms handoff once before the flow can complete.\n")
|
||||
if err := recoverMissingNotify(ctx); err != nil {
|
||||
return in, fmt.Errorf("delegation completed without required notify side effect and recovery failed: notify=%d, want 1: %w", notify, err)
|
||||
}
|
||||
}
|
||||
if notify = notifySvc.count(); notify != 1 {
|
||||
return in, fmt.Errorf("delegation recovery completed without required notify side effect: notify=%d, want 1", notify)
|
||||
}
|
||||
return in, nil
|
||||
}
|
||||
}
|
||||
|
||||
func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notifySvc *NotifyService) error {
|
||||
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 {
|
||||
@@ -503,7 +463,25 @@ func waitForPlanDelegateExecution(done <-chan error, taskSvc *TaskService, notif
|
||||
return fmt.Errorf("flow execute after side effects tasks=%d notify=%d: %w", tasks, notify, err)
|
||||
}
|
||||
if notify != 1 {
|
||||
return fmt.Errorf("delegation completed without required notify side effect: notify=%d, want 1", notify)
|
||||
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)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !settled {
|
||||
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:
|
||||
|
||||
@@ -258,7 +258,7 @@ func TestPlanDelegateExecutionReportsDuplicateNotifyBeforeTimeout(t *testing.T)
|
||||
|
||||
done := make(chan error)
|
||||
errCh := make(chan error, 1)
|
||||
go func() { errCh <- waitForPlanDelegateExecution(done, new(TaskService), notifySvc) }()
|
||||
go func() { errCh <- waitForPlanDelegateExecution(done, new(TaskService), notifySvc, nil) }()
|
||||
|
||||
select {
|
||||
case err := <-errCh:
|
||||
@@ -278,7 +278,7 @@ func TestPlanDelegateExecutionRejectsClaimedCompletionWithoutNotify(t *testing.T
|
||||
done := make(chan error, 1)
|
||||
done <- nil
|
||||
|
||||
err := waitForPlanDelegateExecution(done, new(TaskService), notifySvc)
|
||||
err := waitForPlanDelegateExecution(done, new(TaskService), notifySvc, nil)
|
||||
if err == nil {
|
||||
t.Fatal("waitForPlanDelegateExecution returned nil, want missing notify side-effect error")
|
||||
}
|
||||
@@ -296,13 +296,15 @@ func TestPlanDelegateExecutionRecoversMissingNotifyOnce(t *testing.T) {
|
||||
}
|
||||
}
|
||||
notifySvc := new(NotifyService)
|
||||
done := make(chan error, 1)
|
||||
done <- nil
|
||||
|
||||
recovered := false
|
||||
_, err := requireDelegatedNotifyStep(taskSvc, notifySvc, func(ctx context.Context) error {
|
||||
err := waitForPlanDelegateExecution(done, taskSvc, notifySvc, func(ctx context.Context) error {
|
||||
recovered = true
|
||||
var rsp SendResponse
|
||||
return notifySvc.Send(ctx, &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp)
|
||||
})(context.Background(), flow.State{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("waitForPlanDelegateExecution returned %v, want recovery success", err)
|
||||
}
|
||||
@@ -323,6 +325,8 @@ func TestPlanDelegateExecutionWaitsForInFlightNotifyAfterFlowCompletion(t *testi
|
||||
}
|
||||
}
|
||||
notifySvc := new(NotifyService)
|
||||
done := make(chan error, 1)
|
||||
done <- nil
|
||||
|
||||
go func() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
@@ -331,11 +335,11 @@ func TestPlanDelegateExecutionWaitsForInFlightNotifyAfterFlowCompletion(t *testi
|
||||
}()
|
||||
|
||||
recovered := false
|
||||
_, err := requireDelegatedNotifyStep(taskSvc, notifySvc, func(ctx context.Context) error {
|
||||
err := waitForPlanDelegateExecution(done, taskSvc, notifySvc, func(ctx context.Context) error {
|
||||
recovered = true
|
||||
var rsp SendResponse
|
||||
return notifySvc.Send(ctx, &SendRequest{To: "owner@acme.com", Message: "The launch plan is ready"}, &rsp)
|
||||
})(context.Background(), flow.State{})
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("waitForPlanDelegateExecution returned %v, want in-flight notify success", err)
|
||||
}
|
||||
@@ -370,7 +374,7 @@ func TestPlanDelegateExecutionAcceptsClientTimeoutAfterSideEffects(t *testing.T)
|
||||
done := make(chan error, 1)
|
||||
done <- errors.New(`{"id":"go.micro.client","code":408,"detail":"<nil>","status":"Request Timeout"}`)
|
||||
|
||||
if err := waitForPlanDelegateExecution(done, taskSvc, notifySvc); err != nil {
|
||||
if err := waitForPlanDelegateExecution(done, taskSvc, notifySvc, nil); err != nil {
|
||||
t.Fatalf("waitForPlanDelegateExecution returned %v, want completed side effects to satisfy client timeout", err)
|
||||
}
|
||||
}
|
||||
@@ -379,7 +383,7 @@ func TestPlanDelegateExecutionClassifiesClientTimeoutBeforeSideEffects(t *testin
|
||||
done := make(chan error, 1)
|
||||
done <- errors.New(`{"id":"go.micro.client","code":408,"detail":"<nil>","status":"Request Timeout"}`)
|
||||
|
||||
err := waitForPlanDelegateExecution(done, new(TaskService), new(NotifyService))
|
||||
err := waitForPlanDelegateExecution(done, new(TaskService), new(NotifyService), nil)
|
||||
if err == nil {
|
||||
t.Fatal("waitForPlanDelegateExecution returned nil, want timeout before side effects to fail")
|
||||
}
|
||||
@@ -406,7 +410,7 @@ func TestPlanDelegateExecutionClassifiesPartialClientTimeout(t *testing.T) {
|
||||
done := make(chan error, 1)
|
||||
done <- errors.New(`{"id":"go.micro.client","code":408,"detail":"<nil>","status":"Request Timeout"}`)
|
||||
|
||||
err := waitForPlanDelegateExecution(done, taskSvc, new(NotifyService))
|
||||
err := waitForPlanDelegateExecution(done, taskSvc, new(NotifyService), nil)
|
||||
if err == nil {
|
||||
t.Fatal("waitForPlanDelegateExecution returned nil, want timeout before notify to fail")
|
||||
}
|
||||
|
||||
@@ -17,7 +17,6 @@ func TestZeroToHeroReferenceDocs(t *testing.T) {
|
||||
"go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1",
|
||||
"go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1",
|
||||
"go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1",
|
||||
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
|
||||
"go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1",
|
||||
"./internal/harness/zero-to-hero-ci/run.sh",
|
||||
"go run ./internal/harness/agent-flow",
|
||||
@@ -94,36 +93,6 @@ func TestFirstAgentWayfindingDocs(t *testing.T) {
|
||||
"internal/website/docs/guides/zero-to-hero.md",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "README examples list",
|
||||
file: filepath.Join(root, "README.md"),
|
||||
heading: "## Examples",
|
||||
links: []string{
|
||||
"examples/README.md",
|
||||
"examples/first-agent/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "repository examples index",
|
||||
file: filepath.Join(root, "examples", "README.md"),
|
||||
heading: "## Recommended first-agent path",
|
||||
links: []string{
|
||||
"./first-agent/",
|
||||
"./support/",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "website examples index",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "examples", "index.md"),
|
||||
heading: "## Start here",
|
||||
links: []string{
|
||||
"https://github.com/micro/go-micro/tree/master/examples/first-agent",
|
||||
"../guides/no-secret-first-agent.html",
|
||||
"../guides/your-first-agent.html",
|
||||
"../guides/debugging-agents.html",
|
||||
"../guides/zero-to-hero.html",
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "website getting-started on-ramp",
|
||||
file: filepath.Join(root, "internal", "website", "docs", "getting-started.md"),
|
||||
@@ -160,8 +129,6 @@ func TestNoSecretFirstAgentTranscript(t *testing.T) {
|
||||
guide := readFile(t, filepath.Join(root, "internal", "website", "docs", "guides", "no-secret-first-agent.md"))
|
||||
|
||||
for _, want := range []string{
|
||||
"go run ./examples/first-agent",
|
||||
"go test ./examples/first-agent -run TestRunFirstAgent -count=1",
|
||||
"go run ./examples/support",
|
||||
"go test ./examples/support -run TestRunSupportMockSmoke -count=1",
|
||||
"make harness",
|
||||
@@ -202,23 +169,6 @@ func TestNoSecretFirstAgentTranscript(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFirstAgentWayfindingTargetsExist(t *testing.T) {
|
||||
root := filepath.Clean(filepath.Join("..", "..", ".."))
|
||||
for _, target := range []string{
|
||||
"examples/README.md",
|
||||
"examples/first-agent/README.md",
|
||||
"internal/website/docs/examples/index.md",
|
||||
"internal/website/docs/guides/no-secret-first-agent.md",
|
||||
"internal/website/docs/guides/your-first-agent.md",
|
||||
"internal/website/docs/guides/debugging-agents.md",
|
||||
"internal/website/docs/guides/zero-to-hero.md",
|
||||
} {
|
||||
if _, err := os.Stat(filepath.Join(root, filepath.FromSlash(target))); err != nil {
|
||||
t.Fatalf("first-agent wayfinding target %s disappeared: %v", target, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func firstMarkdownSection(t *testing.T, doc, heading string) string {
|
||||
t.Helper()
|
||||
start := strings.Index(doc, heading)
|
||||
|
||||
@@ -14,6 +14,5 @@ go test ./internal/harness/zero-to-hero-ci -run 'TestNoSecretFirstAgentTranscrip
|
||||
# runtime and mock only the LLM provider. The support example is the maintained
|
||||
# runnable 0→hero app; keep it in this CI path so its documented run/chat/inspect
|
||||
# journey cannot drift from the framework.
|
||||
go test ./examples/first-agent -run TestRunFirstAgent -count=1
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
|
||||
go test ./internal/harness/universe ./internal/harness/plan-delegate -run 'Test.*Harness|TestPlanDelegateEndToEnd|TestPlanDelegateFlowHandoff' -count=1
|
||||
|
||||
@@ -10,12 +10,9 @@ agents → workflows lifecycle.
|
||||
|
||||
## Start here
|
||||
|
||||
For the provider-free first-agent route, run [`examples/first-agent`](https://github.com/micro/go-micro/tree/master/examples/first-agent), then follow [No-secret First Agent](../guides/no-secret-first-agent.html), [Your First Agent](../guides/your-first-agent.html), [Debugging your agent](../guides/debugging-agents.html), and the [0→hero Reference](../guides/zero-to-hero.html).
|
||||
|
||||
| Goal | Runnable example | Why it is useful |
|
||||
| --- | --- | --- |
|
||||
| 0→1 service | [`examples/hello-world`](https://github.com/micro/go-micro/tree/master/examples/hello-world) | Smallest RPC service with a client call and health checks. |
|
||||
| Provider-free first agent | [`examples/first-agent`](https://github.com/micro/go-micro/tree/master/examples/first-agent) | Smallest service-backed agent with a deterministic mock model; no provider key required. |
|
||||
| First service-backed agent | [`examples/agent-demo`](https://github.com/micro/go-micro/tree/master/examples/agent-demo) | Multi-service project/task/team app with agent playground integration. |
|
||||
| 0→hero lifecycle | [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support) | No-secret support-desk story: typed services, an agent, an event-driven flow, and a guardrail. |
|
||||
| Planning and delegation | [`examples/agent-plan-delegate`](https://github.com/micro/go-micro/tree/master/examples/agent-plan-delegate) | Two agents collaborate through `plan` and `delegate` over normal Go Micro RPC. |
|
||||
@@ -28,16 +25,10 @@ For the provider-free first-agent route, run [`examples/first-agent`](https://gi
|
||||
- [Getting Started](../getting-started.html) → run
|
||||
[`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support)
|
||||
to see the full lifecycle before generating your own service.
|
||||
- [No-secret First Agent](../guides/no-secret-first-agent.html) → run
|
||||
[`examples/first-agent`](https://github.com/micro/go-micro/tree/master/examples/first-agent)
|
||||
first for the smallest provider-free agent transcript.
|
||||
- [Your First Agent](../guides/your-first-agent.html) → run
|
||||
[`examples/agent-demo`](https://github.com/micro/go-micro/tree/master/examples/agent-demo)
|
||||
or [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support)
|
||||
when you want a complete service-backed agent to inspect.
|
||||
- [Debugging your agent](../guides/debugging-agents.html) → keep
|
||||
[`examples/first-agent`](https://github.com/micro/go-micro/tree/master/examples/first-agent)
|
||||
nearby as the smallest mock-model reproduction before inspecting richer runs.
|
||||
- [0→hero Reference](../guides/zero-to-hero.html) → run
|
||||
[`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support)
|
||||
for the human-readable scenario, then `make harness` for the full CI contract.
|
||||
|
||||
@@ -25,17 +25,11 @@ end to end with no secrets.
|
||||
|
||||
## Transcript
|
||||
|
||||
From a fresh clone of the repository, first run the smallest service-backed agent:
|
||||
From a fresh clone of the repository:
|
||||
|
||||
```sh
|
||||
git clone https://github.com/micro/go-micro.git
|
||||
cd go-micro
|
||||
go run ./examples/first-agent
|
||||
```
|
||||
|
||||
Then run the maintained support-agent transcript that exercises the full lifecycle:
|
||||
|
||||
```sh
|
||||
go run ./examples/support
|
||||
```
|
||||
|
||||
@@ -62,10 +56,9 @@ trigger and inspect the work.
|
||||
|
||||
## CI-backed check
|
||||
|
||||
Run the same deterministic paths as focused tests:
|
||||
Run the same deterministic path as a focused test:
|
||||
|
||||
```sh
|
||||
go test ./examples/first-agent -run TestRunFirstAgent -count=1
|
||||
go test ./examples/support -run TestRunSupportMockSmoke -count=1
|
||||
```
|
||||
|
||||
|
||||
@@ -24,19 +24,12 @@ cloud credentials?"
|
||||
| Chat | `micro chat` remains the interactive agent entry point. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
| Inspect | `micro inspect agent`, `micro inspect flow`, and `micro flow runs` remain discoverable for run history. | `go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1` |
|
||||
| Deploy | `micro deploy --dry-run` resolves deploy targets without touching remote infrastructure. | `go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1` |
|
||||
| Smallest first agent | `examples/first-agent` runs one service-backed agent with a deterministic mock model and no provider key. | `go test ./examples/first-agent -run TestRunFirstAgent -count=1` |
|
||||
| Runtime reference app | `examples/support` runs typed services, an agent using those services as tools, an event-driven flow handoff, and an approval gate with only the model mocked. | `go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1` |
|
||||
| Runtime harnesses | Real services, agents, durable flows, store-backed history, delegation, and A2A run with only the model mocked. | `./internal/harness/zero-to-hero-ci/run.sh` and `make provider-conformance-mock` |
|
||||
|
||||
## Run the runnable example
|
||||
|
||||
From the repository root, start with the smallest service-backed agent when you want the fastest no-secret success path:
|
||||
|
||||
```sh
|
||||
go run ./examples/first-agent
|
||||
```
|
||||
|
||||
Then run the support-desk example when you want to see the full lifecycle in one terminal:
|
||||
From the repository root, start with the support-desk example when you want to see the full lifecycle in one terminal:
|
||||
|
||||
```sh
|
||||
go run ./examples/support
|
||||
@@ -75,9 +68,6 @@ go test ./cmd/micro -run TestFirstAgentWalkthroughCLIBoundaries -count=1
|
||||
go test ./cmd/micro -run TestZeroToHeroCLIBoundaries -count=1
|
||||
go test ./cmd/micro/cli/deploy -run TestDeployDryRun -count=1
|
||||
|
||||
# Smallest no-secret service-backed first agent.
|
||||
go test ./examples/first-agent -run TestRunFirstAgent -count=1
|
||||
|
||||
# Maintained 0→hero support-desk reference app.
|
||||
go test ./examples/support -run 'TestRunSupportMockSmoke|TestZeroToHeroReadmeDocumentsLifecycle' -count=1
|
||||
|
||||
@@ -93,9 +83,6 @@ make provider-conformance-mock
|
||||
|
||||
## Reference scenarios
|
||||
|
||||
- [`examples/first-agent`](https://github.com/micro/go-micro/tree/master/examples/first-agent)
|
||||
is the smallest no-secret service-backed agent: one notes service, one scoped
|
||||
assistant agent, and a deterministic mock model.
|
||||
- [`examples/support`](https://github.com/micro/go-micro/tree/master/examples/support)
|
||||
is the runnable support-desk story: customers, tickets, notify, a support
|
||||
agent, an intake flow, and an approval gate in one no-secret example.
|
||||
|
||||
Reference in New Issue
Block a user