Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5134931260 | |||
| 5724f0a94b | |||
| e071084ebe |
@@ -22,8 +22,6 @@ below is kept current between tags and rolled into the next version when it ship
|
||||
- **Model retry jitter controls** — model retry behavior can now use jitter controls to reduce synchronized retry bursts. (`ai/`, `agent/`)
|
||||
- **Compacted memory summaries** — agent memory now exposes compacted run summaries for easier inspection and recovery. (`agent/`)
|
||||
- **CLI input resume for agent runs** — the CLI can resume agent runs that require additional user input. (`cmd/micro/`, `agent/`)
|
||||
- **A2A inbound AP2 mandate verification (opt-in)** — set `Options.AP2PublicKey` (or `a2a.WithPushURLPolicy`'s sibling `a2a.WithAP2PublicKey` for embedded handlers) and the gateway verifies AP2 payment/checkout mandates carried on incoming messages — signature and task/context binding — recording the outcome in each task's `ap2Verifications`, with the x402 settlement rail carried through for the paid path. Off by default; mandates are otherwise carried unverified. (`gateway/a2a/`)
|
||||
- **Flow human-in-the-loop pause/resume** — a flow step can suspend a run for external input with `flow.Await(key, prompt)` (or `flow.AwaitStep`): the run checkpoints with status `waiting` and `Execute` returns cleanly. `Flow.Waiting` lists suspended runs with what they await, and `Flow.ResumeWith(ctx, runID, input)` injects the input and continues from the next step. Recovery (`ResumePending`) skips waiting runs since they need input, not a restart. (`flow/`)
|
||||
|
||||
### Changed
|
||||
- **Remote agent chat streaming** — `micro chat` now streams replies from remote agents instead of waiting for the full response. (`cmd/micro/`, `agent/`)
|
||||
@@ -34,7 +32,6 @@ below is kept current between tags and rolled into the next version when it ship
|
||||
|
||||
### Security
|
||||
- **x402 spend-cap hardening** — the paying `Client` now refuses a 402 whose `maxAmountRequired` is not a positive integer (a swallowed parse error or negative amount previously bypassed the budget cap), and a new `Config.RequireSettlement` fails closed when a paid request is served by a verify-only facilitator that never captures funds. (`wrapper/x402/`)
|
||||
- **A2A push-notification SSRF guard** — the A2A gateway no longer delivers task push notifications to caller-supplied URLs that resolve to loopback, private, link-local (incl. cloud metadata), or unspecified addresses. Callbacks are validated when set and re-checked at dial time on the resolved IP (DNS-rebinding safe); non-http(s) schemes are rejected. `Options.AllowPushURL` (and `a2a.WithPushURLPolicy` for embedded handlers) lets operators authorize trusted in-cluster receivers. (`gateway/a2a/`)
|
||||
|
||||
---
|
||||
|
||||
|
||||
+514
@@ -0,0 +1,514 @@
|
||||
# Go Micro [](https://pkg.go.dev/go-micro.dev/v6?tab=doc) [](https://discord.gg/G8Gk5j3uXr)
|
||||
|
||||
Go Micro is an **agent harness** and service framework for Go.
|
||||
|
||||
**Community:** questions, ideas, or just want to build alongside us? [Join the Discord](https://discord.gg/G8Gk5j3uXr).
|
||||
|
||||
A harness is the runtime around an agent: the tools it can call, the memory it keeps, the guardrails that bound it, the workflows that trigger it, the services it depends on, and the protocols other agents use to reach it.
|
||||
|
||||
Go Micro gives you the harness as Go code. Build an agent and it gets a model, memory, tools, planning, delegation, guardrails, and service discovery; it is reachable over [MCP](https://modelcontextprotocol.io/) and [A2A](https://a2a-protocol.org). Write services and every endpoint becomes an AI-callable tool. Orchestrate the deterministic parts with durable flows. Agents, services, and flows share one runtime because an agent is a distributed system, and building one is building a service.
|
||||
|
||||
## Sponsors
|
||||
|
||||
<a href="https://go-micro.dev/blog/3"><img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" height="26" /></a>
|
||||
|
||||
<a href="https://go-micro.dev/blog/29"><img src="https://upload.wikimedia.org/wikipedia/commons/4/4d/OpenAI_Logo.svg" height="26" /></a>
|
||||
|
||||
<a href="https://go-micro.dev/blog/8"><img src="https://www.atlascloud.ai/logo.svg" height="26" /></a>
|
||||
|
||||
**Want to support Go Micro and see your logo here?** [Become a sponsor](https://discord.gg/G8Gk5j3uXr) — reach out on Discord.
|
||||
|
||||
## Commercial Support
|
||||
|
||||
Running Go Micro in production, or building on it and want help? Paid **support, consulting, training, and retainers** are available directly from the maintainer — and they're what keep the project maintained. See [**Support**](SUPPORT.md) for the tiers, or [open a request](https://github.com/micro/go-micro/issues/new?template=commercial_support.md).
|
||||
|
||||
## Contents
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [First agent on-ramp](#first-agent-on-ramp)
|
||||
- [Why an Agent Harness](#why-an-agent-harness)
|
||||
- [Writing Services](#writing-services)
|
||||
- [Building Agents](#building-agents) — [Plan & Delegate](#plan--delegate), [Pluggable](#batteries-included-pluggable), [Paid tools (x402)](#paid-tools-x402), [A2A](#reachable-by-other-agents-a2a)
|
||||
- [Features](#features)
|
||||
- [CLI](#cli)
|
||||
- [Autonomous improvement loop](#autonomous-improvement-loop)
|
||||
- [Multi-Service Projects](#multi-service-projects)
|
||||
- [Data Model](#data-model)
|
||||
- [AI Providers](#ai-providers)
|
||||
- [Examples](#examples)
|
||||
- [Commercial Support](#commercial-support)
|
||||
- [Docs](#docs)
|
||||
|
||||
## Quick Start
|
||||
|
||||
Install the CLI:
|
||||
|
||||
```bash
|
||||
# Binary (no Go required)
|
||||
curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
|
||||
# Or with Go
|
||||
go install go-micro.dev/v6/cmd/micro@latest
|
||||
```
|
||||
|
||||
If install or `PATH` checks fail, use the [install troubleshooting guide](internal/website/docs/guides/install-troubleshooting.md) before scaffolding your first service.
|
||||
|
||||
### Fastest start — no API key
|
||||
|
||||
Scaffold a service, run it, call it:
|
||||
|
||||
```bash
|
||||
micro new helloworld
|
||||
cd helloworld
|
||||
micro run
|
||||
```
|
||||
|
||||
Then in another terminal:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
|
||||
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
||||
```
|
||||
|
||||
This install → scaffold → run → call path is covered by no-secret CI harnesses. To
|
||||
verify just the local installer and first-run CLI boundaries without network
|
||||
access or provider keys, use:
|
||||
|
||||
```bash
|
||||
make install-smoke
|
||||
```
|
||||
|
||||
To verify the focused CLI inner-loop contract — scaffold → run/chat/inspect → deploy dry-run — use:
|
||||
|
||||
```bash
|
||||
make inner-loop
|
||||
```
|
||||
|
||||
To run only the ordered [0→hero services → agents → workflows transcript](internal/website/docs/guides/zero-to-hero.md) that CI guards, use:
|
||||
|
||||
```bash
|
||||
make zero-to-hero-transcript
|
||||
```
|
||||
|
||||
To run the broader local contract (including that transcript, chat/inspect CLI boundaries, and deploy dry-run), use:
|
||||
|
||||
```bash
|
||||
make harness
|
||||
```
|
||||
|
||||
### First agent on-ramp
|
||||
|
||||
After install and the first `micro new`/`micro run` smoke check, take the
|
||||
walkable agent path in this order:
|
||||
|
||||
1. [Install troubleshooting](internal/website/docs/guides/install-troubleshooting.md) — verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path before agent work.
|
||||
|
||||
Run `make docs-wayfinding` to verify the focused no-secret docs/CLI contract that keeps these README and website commands aligned with the installed CLI.
|
||||
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
|
||||
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
6. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
|
||||
7. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
|
||||
8. [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.
|
||||
9. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
service-backed agent and talk to it with `micro chat`.
|
||||
10. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
|
||||
`micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`,
|
||||
then `micro chat` and `micro inspect agent <name>` to recover run history, memory,
|
||||
and provider checks when the first conversation does something unexpected.
|
||||
11. [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.
|
||||
|
||||
### Autonomous improvement loop
|
||||
|
||||
Want the same services → agents → workflows lifecycle applied to your
|
||||
repository? `micro loop` scaffolds the autonomous improvement loop used by Go
|
||||
Micro itself: a North Star, ranked issue queue, role prompts, GitHub Actions
|
||||
workflows, and verification for CI-gated PRs.
|
||||
|
||||
```bash
|
||||
micro loop init --roles all
|
||||
micro loop verify
|
||||
```
|
||||
|
||||
Before turning on the schedule, configure a dispatch token such as
|
||||
`CODEX_TRIGGER_TOKEN`, protect the default branch with required CI checks
|
||||
(`go build ./...`, `go test ./...`, and `golangci-lint run ./...` for this
|
||||
repository), and seed `.github/loop/PRIORITIES.md` with one scoped issue per
|
||||
increment. See the [`micro loop` quickstart](internal/website/docs/guides/micro-loop.md)
|
||||
for the setup checklist and operating model.
|
||||
|
||||
### Generate from a prompt — with an LLM key
|
||||
|
||||
Set a provider key, describe what you want, and the AI designs services, writes handlers, compiles, and starts them:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
||||
micro run --prompt "a task management system with categories" --provider anthropic
|
||||
```
|
||||
|
||||
The AI designs the architecture, you review it, then it generates handlers with real business logic, compiles them, and starts them:
|
||||
|
||||
```
|
||||
Services:
|
||||
● task — Task management with status tracking
|
||||
● project — Project organization
|
||||
|
||||
Generate? [Y/n]
|
||||
|
||||
Micro
|
||||
Services:
|
||||
● task
|
||||
● project
|
||||
Agents:
|
||||
◆ agent
|
||||
```
|
||||
|
||||
Then talk to your services from the console:
|
||||
|
||||
```
|
||||
> Create a project called Launch, then add three tasks to it
|
||||
|
||||
→ project_Project_Create({"name":"Launch"})
|
||||
← {"record":{"id":"p1..."},"success":true}
|
||||
→ task_Task_Create({"title":"Design specs","project_id":"p1..."})
|
||||
→ task_Task_Create({"title":"Write code","project_id":"p1..."})
|
||||
→ task_Task_Create({"title":"Ship it","project_id":"p1..."})
|
||||
|
||||
Created project Launch and added three tasks to it.
|
||||
```
|
||||
|
||||
When you need a capability that doesn't exist, the agent generates a new service mid-conversation:
|
||||
|
||||
```
|
||||
> I need to track shipping. Create a shipment for order 123 to London.
|
||||
|
||||
⚡ generating shipping service...
|
||||
✓ shipping
|
||||
→ shipping_Shipping_Create({"order_id":"123","destination":"London"})
|
||||
← {"record":{"id":"xyz...","status":"pending"}}
|
||||
|
||||
Created shipment for order 123 going to London.
|
||||
```
|
||||
|
||||
Edit the generated code by hand at any time — re-running preserves your changes. [Read more](https://go-micro.dev/blog/13).
|
||||
|
||||
## Why an Agent Harness
|
||||
|
||||
The first wave of agent frameworks helped developers put a model in a loop. The next problem is operating that loop: connecting it to real tools, scoping what it can touch, preserving state, routing work to specialists, recovering from failures, observing what happened, and letting other agents call it. That is harness work.
|
||||
|
||||
Go Micro's answer is to make the harness the same thing you already deploy:
|
||||
|
||||
- **Tools are services** — endpoint metadata becomes tool schema; RPC executes the call.
|
||||
- **Agents are services** — they register, discover, load-balance, and expose `Agent.Chat`.
|
||||
- **Workflows are durable code paths** — use flows when the path is known; dispatch to agents when it is not.
|
||||
- **Safety lives at execution** — `MaxSteps`, `LoopLimit`, `ApproveTool`, and tool wrappers run where actions happen.
|
||||
- **Interop is built in** — MCP for tools, A2A for agents, x402 for paid tools.
|
||||
|
||||
Use Go Micro when the agent has to operate a system, not just answer a prompt.
|
||||
|
||||
## Writing Services
|
||||
|
||||
Under the hood, a service is a struct with methods. Doc comments and `@example` tags become tool descriptions for AI agents automatically.
|
||||
|
||||
```go
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go-micro.dev/v6"
|
||||
)
|
||||
|
||||
type Request struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type Say struct{}
|
||||
|
||||
// Hello greets a person by name.
|
||||
// @example {"name": "Alice"}
|
||||
func (h *Say) Hello(ctx context.Context, req *Request, rsp *Response) error {
|
||||
rsp.Message = "Hello " + req.Name
|
||||
return nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
service := micro.NewService("greeter")
|
||||
service.Handle(new(Say))
|
||||
service.Run()
|
||||
}
|
||||
```
|
||||
|
||||
Run it and everything is accessible — REST, gRPC, MCP, agent playground:
|
||||
|
||||
```bash
|
||||
micro run
|
||||
# Dashboard: http://localhost:8080
|
||||
# API: http://localhost:8080/api/{service}/{method}
|
||||
# Agent: http://localhost:8080/agent
|
||||
# MCP Tools: http://localhost:8080/mcp/tools
|
||||
```
|
||||
|
||||
You can also scaffold a service from a template:
|
||||
|
||||
```bash
|
||||
micro new helloworld
|
||||
micro new contacts --template crud
|
||||
```
|
||||
|
||||
## Building Agents
|
||||
|
||||
An Agent is a service with an LLM inside it. It has a proto-defined `Agent.Chat` RPC endpoint, registers in the registry, and is callable like any service:
|
||||
|
||||
```go
|
||||
agent := micro.NewAgent("task-mgr",
|
||||
micro.AgentServices("task", "project"),
|
||||
micro.AgentPrompt("You manage tasks and projects. You understand deadlines and priorities."),
|
||||
micro.AgentProvider("anthropic"),
|
||||
)
|
||||
agent.Run()
|
||||
```
|
||||
|
||||
The agent discovers its services from the registry, scopes its tools to their endpoints, and maintains conversation memory in the store. It registers itself so `micro chat` and other agents can find it.
|
||||
|
||||
```go
|
||||
// Programmatic interaction
|
||||
resp, _ := agent.Ask(ctx, "What tasks are overdue?")
|
||||
fmt.Println(resp.Reply)
|
||||
```
|
||||
|
||||
Multiple agents coordinate via RPC — each is a service with an `Agent.Chat` endpoint. `micro chat` routes to the right one.
|
||||
|
||||
```bash
|
||||
micro agent list # list registered agents
|
||||
micro call task-mgr Agent.Chat '{"message": "What tasks are overdue?"}'
|
||||
```
|
||||
|
||||
### Plan & Delegate
|
||||
|
||||
Every agent gets two built-in harness capabilities, exposed as tools — no extra setup or separate graph runtime:
|
||||
|
||||
- **`plan`** — for multi-step work, the agent records an ordered plan in its store-backed memory and stays oriented across turns.
|
||||
- **`delegate`** — the agent hands a self-contained subtask to another agent. If a registered agent already owns the relevant services, the hand-off goes over RPC to that agent; otherwise a focused, short-lived sub-agent is created for the subtask with its own isolated context.
|
||||
|
||||
This keeps intelligence distributed: an agent doesn't need to know *how* to do everything, only *who* does. See [examples/agent-plan-delegate](examples/agent-plan-delegate/).
|
||||
|
||||
```go
|
||||
// A sub-agent is just an agent — created with New, talked to with Ask.
|
||||
// delegate-first: reuse a registered agent, or spin up a focused one.
|
||||
resp, _ := agent.Ask(ctx, "Plan the launch, create the tasks, and have comms notify the owner.")
|
||||
```
|
||||
|
||||
### Batteries included, pluggable
|
||||
|
||||
Just as a service composes pluggable abstractions (registry, broker, store), an agent composes a **model**, **memory**, and **tools** — sane defaults out of the box, each swappable.
|
||||
|
||||
```go
|
||||
agent := micro.NewAgent("assistant",
|
||||
micro.AgentProvider("anthropic"), // model — swap the provider
|
||||
micro.AgentCompactMemory(40, 12), // memory — durable, summarized, recallable
|
||||
micro.AgentTool("weather", "Get the weather for a city",
|
||||
map[string]any{"city": map[string]any{"type": "string"}},
|
||||
func(ctx context.Context, in map[string]any) (string, error) {
|
||||
return getWeather(in["city"].(string)) // tools beyond your services — any function
|
||||
}),
|
||||
micro.AgentMaxSteps(8), // guardrails
|
||||
)
|
||||
```
|
||||
|
||||
**Memory** is durable and store-backed by default (Postgres, NATS KV, or file), so an agent picks up where it left off after a restart — or supply your own with `AgentMemory`. Long-running agents can opt into `AgentCompactMemory(maxMessages, keepRecent)`: older turns are collapsed into a deterministic summary, recent turns stay verbatim, and relevant archived turns are recalled on future asks without replaying the whole conversation. **Tools** are your services automatically, plus any function you register with `AgentTool`.
|
||||
|
||||
### Paid tools (x402)
|
||||
|
||||
Every endpoint is an AI-callable tool — and it can be a *paid* tool. Go Micro supports [x402](https://x402.org), the HTTP 402 payment standard for agents, so a tool can require a stablecoin payment and an agent can settle it autonomously. It's opt-in and carries no crypto in the framework: verification is delegated to a pluggable facilitator (Coinbase, Alchemy, self-hosted), so Base and Solana are just different facilitators.
|
||||
|
||||
```bash
|
||||
# Charge for tool calls at the MCP gateway (off unless you set a pay-to address)
|
||||
micro mcp serve --x402_pay_to 0xYourAddress --x402_network solana --x402_amount 10000
|
||||
# Per-tool amounts via a config file
|
||||
micro mcp serve --x402_config x402.json
|
||||
```
|
||||
|
||||
See the [Payments (x402) guide](internal/website/docs/guides/x402-payments.md).
|
||||
|
||||
### Reachable by other agents (A2A)
|
||||
|
||||
Within a Go Micro system, agents reach each other over RPC. To make them reachable by agents on *other* frameworks, Go Micro speaks the [Agent2Agent (A2A) protocol](https://a2a-protocol.org). The A2A gateway discovers your agents from the registry, generates an Agent Card for each from its metadata — the same way the MCP gateway derives tools from service endpoints — and translates incoming A2A tasks to the agent's `Agent.Chat` RPC. No per-agent code: register an agent and it's reachable over A2A.
|
||||
|
||||
```bash
|
||||
micro a2a serve --address :4000 # gateway: expose every registered agent over A2A
|
||||
micro a2a list # agents and their Agent Card URLs
|
||||
```
|
||||
|
||||
Or skip the gateway entirely — an agent can serve its own A2A endpoint directly, handling tasks in-process:
|
||||
|
||||
```go
|
||||
micro.NewAgent("task-mgr", micro.AgentServices("task"), micro.AgentA2A(":4000"))
|
||||
```
|
||||
|
||||
It works both ways. To call an agent on another framework, an `a2a.Client` is wired into the two places that hand off work: `flow.A2A(url)` as a workflow step (the cross-framework `Dispatch`), and `delegate` to an `http(s)` URL from inside an agent.
|
||||
|
||||
MCP exposes your services as tools; A2A exposes your agents as agents. See the [A2A guide](internal/website/docs/guides/a2a-protocol.md).
|
||||
|
||||
## Features
|
||||
|
||||
### AI
|
||||
|
||||
| Feature | Details |
|
||||
|---------|---------|
|
||||
| Agents | `micro.NewAgent()` — intelligent layer that manages services |
|
||||
| Plan & delegate | Built-in agent tools — plan multi-step work, delegate subtasks to other agents |
|
||||
| Pluggable memory | Durable store-backed conversation memory by default; swap with `AgentMemory` |
|
||||
| Custom tools | `AgentTool` — give an agent any function as a tool, beyond its services |
|
||||
| Guardrails | `MaxSteps` (stop on count), `LoopLimit` (stop repeated no-progress calls), `ApproveTool` (human-in-the-loop) |
|
||||
| Tool middleware | `AgentWrapTool` — wrap tool execution for logging, metrics, or retries (like client/server wrappers) |
|
||||
| Workflows | `micro.NewFlow()` — event-driven; one step, ordered durable steps, or triggers an agent |
|
||||
| Durable execution | Checkpointed flow steps survive a crash and resume where they stopped; store-backed by default, pluggable backend |
|
||||
| MCP gateway | Every endpoint is an AI tool automatically |
|
||||
| A2A gateway | Every agent is reachable over the Agent2Agent protocol; cards generated from the registry (`micro a2a`) |
|
||||
| Payments (x402) | Opt-in per-call payments for tools via the x402 standard; pluggable facilitator (Base, Solana, …) |
|
||||
| 9 LLM providers | Anthropic, OpenAI, Gemini, Groq, Mistral, Together, Atlas Cloud, MiniMax, Ollama (local + cloud) |
|
||||
| Interactive console | `micro run` includes a chat console for talking to services |
|
||||
| Service generation | `micro run --prompt` — describe a system, get running services |
|
||||
|
||||
### Framework
|
||||
|
||||
| Feature | Details |
|
||||
|---------|---------|
|
||||
| Service registry | mDNS (default), Consul, etcd |
|
||||
| RPC client/server | gRPC transport, load balancing, streaming |
|
||||
| Pub/sub events | NATS, RabbitMQ, HTTP broker |
|
||||
| Key-value store | File (bbolt), Postgres, NATS KV |
|
||||
| Typed model layer | CRUD + queries, SQLite/Postgres backends |
|
||||
| Everything swappable | All abstractions are Go interfaces |
|
||||
|
||||
### Developer experience & deployment
|
||||
|
||||
| Feature | Details |
|
||||
|---------|---------|
|
||||
| Hot reload | `micro run` watches files, rebuilds on change |
|
||||
| Templates | `micro new --template crud/pubsub/api` |
|
||||
| One-command deploy | `micro deploy user@server` — SSH + systemd, no Docker |
|
||||
|
||||
## CLI
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `micro run --prompt "..."` | Generate services + agent, start with interactive console |
|
||||
| `micro run` | Dev mode: hot reload, gateway, interactive console |
|
||||
| `micro run -d` | Detached mode (no console) |
|
||||
| `micro chat` | Standalone chat (when not using micro run) |
|
||||
| `micro agent list` | List registered agents |
|
||||
| `micro new myservice` | Scaffold a service |
|
||||
| `micro call service endpoint '{}'` | Call a service or agent from the CLI |
|
||||
| `micro build` | Compile production binaries |
|
||||
| `micro deploy user@server` | Deploy via SSH + systemd |
|
||||
|
||||
## Multi-Service Projects
|
||||
|
||||
Run multiple services together:
|
||||
|
||||
```go
|
||||
users := micro.NewService("users", micro.Address(":9001"))
|
||||
orders := micro.NewService("orders", micro.Address(":9002"))
|
||||
|
||||
users.Handle(new(Users))
|
||||
orders.Handle(new(Orders))
|
||||
|
||||
g := micro.NewGroup(users, orders)
|
||||
g.Run()
|
||||
```
|
||||
|
||||
Or use a `micro.mu` config file:
|
||||
|
||||
```
|
||||
service users
|
||||
path ./users
|
||||
|
||||
service orders
|
||||
path ./orders
|
||||
depends users
|
||||
```
|
||||
|
||||
## Data Model
|
||||
|
||||
Typed persistence with CRUD and queries:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
ID string `json:"id" model:"key"`
|
||||
Name string `json:"name"`
|
||||
Email string `json:"email" model:"index"`
|
||||
}
|
||||
|
||||
db := service.Model()
|
||||
db.Register(&User{})
|
||||
db.Create(ctx, &User{ID: "1", Name: "Alice", Email: "alice@example.com"})
|
||||
|
||||
var results []*User
|
||||
db.List(ctx, &results, model.Where("email", "alice@example.com"))
|
||||
```
|
||||
|
||||
Backends: memory (default), SQLite, Postgres.
|
||||
|
||||
## AI Providers
|
||||
|
||||
Swap providers with a single import — same interface everywhere:
|
||||
|
||||
| Provider | Default Model |
|
||||
|----------|---------------|
|
||||
| Anthropic | `claude-sonnet-4-20250514` |
|
||||
| OpenAI | `gpt-4o` |
|
||||
| Google Gemini | `gemini-2.5-flash` |
|
||||
| Groq | `llama-3.3-70b-versatile` |
|
||||
| Mistral | `mistral-large-latest` |
|
||||
| Together AI | `meta-llama/Llama-3.3-70B-Instruct-Turbo` |
|
||||
| Atlas Cloud | `deepseek-ai/DeepSeek-V3-0324` |
|
||||
| MiniMax | `MiniMax-M3` |
|
||||
| Ollama | `llama3.2` (local) |
|
||||
|
||||
```go
|
||||
m := ai.New("anthropic", ai.WithAPIKey(key))
|
||||
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
|
||||
|
||||
See [all examples](examples/README.md).
|
||||
|
||||
## Docs
|
||||
|
||||
- [Getting Started](internal/website/docs/getting-started.md)
|
||||
- [AI Integration](internal/website/docs/ai-integration.md)
|
||||
- [Your First Agent](internal/website/docs/guides/your-first-agent.md)
|
||||
- [0→hero Reference](internal/website/docs/guides/zero-to-hero.md)
|
||||
- [Agents and Workflows](internal/website/docs/guides/agents-and-workflows.md)
|
||||
- [Agent Design](internal/docs/AGENT_DESIGN.md)
|
||||
- [Plan & Delegate](internal/website/docs/guides/plan-delegate.md)
|
||||
- [Agent Guardrails](internal/website/docs/guides/agent-guardrails.md)
|
||||
- [Payments (x402)](internal/website/docs/guides/x402-payments.md)
|
||||
- [MCP & AI Agents](internal/website/docs/mcp.md)
|
||||
- [Data Model](internal/website/docs/model.md)
|
||||
- [Deployment](internal/website/docs/deployment.md)
|
||||
- [Plugins](internal/website/docs/plugins.md)
|
||||
|
||||
Package reference: https://pkg.go.dev/go-micro.dev/v6
|
||||
@@ -1,14 +1,20 @@
|
||||
<!-- WEHUB_ZH_README -->
|
||||
> [!NOTE]
|
||||
> 本文档由 WeHub 基于上游 README 翻译整理,属于社区翻译,非官方中文文档。
|
||||
> [English](./README.en.md) · [原始项目](https://github.com/micro/go-micro) · [上游 README](https://github.com/micro/go-micro/blob/HEAD/README.md)
|
||||
> 原作者、版权与许可证归属以原始项目及本仓库 LICENSE 文件为准。
|
||||
|
||||
# Go Micro [](https://pkg.go.dev/go-micro.dev/v6?tab=doc) [](https://discord.gg/G8Gk5j3uXr)
|
||||
|
||||
Go Micro is an **agent harness** and service framework for Go.
|
||||
Go Micro 是一个面向 Go 的 **agent harness(智能体运行时框架)** 与服务框架。
|
||||
|
||||
**Community:** questions, ideas, or just want to build alongside us? [Join the Discord](https://discord.gg/G8Gk5j3uXr).
|
||||
**社区:** 有问题、有想法,或想与我们一起构建?[加入 Discord](https://discord.gg/G8Gk5j3uXr).
|
||||
|
||||
A harness is the runtime around an agent: the tools it can call, the memory it keeps, the guardrails that bound it, the workflows that trigger it, the services it depends on, and the protocols other agents use to reach it.
|
||||
Harness 是围绕智能体的运行时:它能调用的工具、它保留的记忆、约束它的护栏、触发它的工作流、它依赖的服务,以及其他智能体与之通信所使用的协议。
|
||||
|
||||
Go Micro gives you the harness as Go code. Build an agent and it gets a model, memory, tools, planning, delegation, guardrails, and service discovery; it is reachable over [MCP](https://modelcontextprotocol.io/) and [A2A](https://a2a-protocol.org). Write services and every endpoint becomes an AI-callable tool. Orchestrate the deterministic parts with durable flows. Agents, services, and flows share one runtime because an agent is a distributed system, and building one is building a service.
|
||||
Go Micro 以 Go 代码的形式提供该 harness。构建一个智能体即可获得模型、记忆、工具、规划、委派、护栏与服务发现;可通过 [MCP](https://modelcontextprotocol.io/) 和 [A2A](https://a2a-protocol.org). 被访问。编写服务后,每个端点都会成为 AI 可调用的工具。用持久化 flow 编排确定性部分。智能体、服务与 flow 共享同一运行时,因为智能体本质上是一个分布式系统,构建智能体就是在构建服务。
|
||||
|
||||
## Sponsors
|
||||
## 赞助商
|
||||
|
||||
<a href="https://go-micro.dev/blog/3"><img src="https://upload.wikimedia.org/wikipedia/commons/7/78/Anthropic_logo.svg" height="26" /></a>
|
||||
|
||||
@@ -16,32 +22,32 @@ Go Micro gives you the harness as Go code. Build an agent and it gets a model, m
|
||||
|
||||
<a href="https://go-micro.dev/blog/8"><img src="https://www.atlascloud.ai/logo.svg" height="26" /></a>
|
||||
|
||||
**Want to support Go Micro and see your logo here?** [Become a sponsor](https://discord.gg/G8Gk5j3uXr) — reach out on Discord.
|
||||
**想支持 Go Micro 并在此展示你的 logo?** [成为赞助商](https://discord.gg/G8Gk5j3uXr) — 可通过 Discord 联系。
|
||||
|
||||
## Commercial Support
|
||||
## 商业支持
|
||||
|
||||
Running Go Micro in production, or building on it and want help? Paid **support, consulting, training, and retainers** are available directly from the maintainer — and they're what keep the project maintained. See [**Support**](SUPPORT.md) for the tiers, or [open a request](https://github.com/micro/go-micro/issues/new?template=commercial_support.md).
|
||||
要在生产环境运行 Go Micro,或基于它进行开发并需要帮助?可直接向维护者购买 **支持、咨询、培训与 retainer(长期服务)** — 这些也是项目得以持续维护的支撑。详见 [**Support**](SUPPORT.md) 中的层级说明,或 [提交请求](https://github.com/micro/go-micro/issues/new?template=commercial_support.md).
|
||||
|
||||
## Contents
|
||||
## 目录
|
||||
|
||||
- [Quick Start](#quick-start)
|
||||
- [First agent on-ramp](#first-agent-on-ramp)
|
||||
- [Why an Agent Harness](#why-an-agent-harness)
|
||||
- [Writing Services](#writing-services)
|
||||
- [Building Agents](#building-agents) — [Plan & Delegate](#plan--delegate), [Pluggable](#batteries-included-pluggable), [Paid tools (x402)](#paid-tools-x402), [A2A](#reachable-by-other-agents-a2a)
|
||||
- [Features](#features)
|
||||
- [快速开始](#quick-start)
|
||||
- [首个智能体入门路径](#first-agent-on-ramp)
|
||||
- [为何需要 Agent Harness](#why-an-agent-harness)
|
||||
- [编写服务](#writing-services)
|
||||
- [构建智能体](#building-agents) — [规划与委派](#plan--delegate)、[可插拔](#batteries-included-pluggable)、[付费工具 (x402)](#paid-tools-x402)、[A2A](#reachable-by-other-agents-a2a)
|
||||
- [功能特性](#features)
|
||||
- [CLI](#cli)
|
||||
- [Autonomous improvement loop](#autonomous-improvement-loop)
|
||||
- [Multi-Service Projects](#multi-service-projects)
|
||||
- [Data Model](#data-model)
|
||||
- [AI Providers](#ai-providers)
|
||||
- [Examples](#examples)
|
||||
- [Commercial Support](#commercial-support)
|
||||
- [Docs](#docs)
|
||||
- [自主改进循环](#autonomous-improvement-loop)
|
||||
- [多服务项目](#multi-service-projects)
|
||||
- [数据模型](#data-model)
|
||||
- [AI 提供商](#ai-providers)
|
||||
- [示例](#examples)
|
||||
- [商业支持](#commercial-support)
|
||||
- [文档](#docs)
|
||||
|
||||
## Quick Start
|
||||
## 快速开始
|
||||
|
||||
Install the CLI:
|
||||
安装 CLI:
|
||||
|
||||
```bash
|
||||
# Binary (no Go required)
|
||||
@@ -51,11 +57,11 @@ curl -fsSL https://go-micro.dev/install.sh | sh
|
||||
go install go-micro.dev/v6/cmd/micro@latest
|
||||
```
|
||||
|
||||
If install or `PATH` checks fail, use the [install troubleshooting guide](internal/website/docs/guides/install-troubleshooting.md) before scaffolding your first service.
|
||||
如果安装或 `PATH` 检查失败,在搭建第一个服务之前,请先参阅 [安装故障排除指南](internal/website/docs/guides/install-troubleshooting.md)。
|
||||
|
||||
### Fastest start — no API key
|
||||
### 最快上手 — 无需 API key
|
||||
|
||||
Scaffold a service, run it, call it:
|
||||
搭建服务、运行并调用:
|
||||
|
||||
```bash
|
||||
micro new helloworld
|
||||
@@ -63,95 +69,85 @@ cd helloworld
|
||||
micro run
|
||||
```
|
||||
|
||||
Then in another terminal:
|
||||
然后在另一个终端中:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:8080/api/helloworld/Helloworld.Call \
|
||||
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
||||
```
|
||||
|
||||
This install → scaffold → run → call path is covered by no-secret CI harnesses. To
|
||||
verify just the local installer and first-run CLI boundaries without network
|
||||
access or provider keys, use:
|
||||
这条 安装 → 搭建 → 运行 → 调用 路径由无密钥 CI harness 覆盖。若要在无网络访问或提供商密钥的情况下,仅验证本地安装器与首次运行 CLI 边界,请使用:
|
||||
|
||||
```bash
|
||||
make install-smoke
|
||||
```
|
||||
|
||||
To verify the focused CLI inner-loop contract — scaffold → run/chat/inspect → deploy dry-run — use:
|
||||
若要验证聚焦的 CLI 内循环契约 — 搭建 → 运行/聊天/检查 → 部署 dry-run — 请使用:
|
||||
|
||||
```bash
|
||||
make inner-loop
|
||||
```
|
||||
|
||||
To run only the ordered [0→hero services → agents → workflows transcript](internal/website/docs/guides/zero-to-hero.md) that CI guards, use:
|
||||
若仅运行 CI 守护的有序 [0→hero 服务 → 智能体 → 工作流 脚本](internal/website/docs/guides/zero-to-hero.md),请使用:
|
||||
|
||||
```bash
|
||||
make zero-to-hero-transcript
|
||||
```
|
||||
|
||||
To run the broader local contract (including that transcript, chat/inspect CLI boundaries, and deploy dry-run), use:
|
||||
若要运行更广泛的本地契约(包括该脚本、聊天/检查 CLI 边界以及部署 dry-run),请使用:
|
||||
|
||||
```bash
|
||||
make harness
|
||||
```
|
||||
|
||||
### First agent on-ramp
|
||||
### 首个智能体入门路径
|
||||
|
||||
After install and the first `micro new`/`micro run` smoke check, take the
|
||||
walkable agent path in this order:
|
||||
完成安装并进行首次 `micro new`/`micro run` 冒烟检查后,按以下顺序走可逐步跟进的智能体路径:
|
||||
|
||||
1. [Install troubleshooting](internal/website/docs/guides/install-troubleshooting.md) — verify the binary installer or `go install`, `PATH`, `micro --version`, and the no-secret smoke path before agent work.
|
||||
1. [安装故障排除](internal/website/docs/guides/install-troubleshooting.md) — 在开始智能体相关工作前,验证二进制安装器或 `go install`、`PATH`、`micro --version`,以及无密钥冒烟路径。
|
||||
|
||||
Run `make docs-wayfinding` to verify the focused no-secret docs/CLI contract that keeps these README and website commands aligned with the installed CLI.
|
||||
运行 `make docs-wayfinding` 以验证聚焦的无密钥 docs/CLI 契约,确保本 README 与网站命令与已安装的 CLI 保持一致。
|
||||
|
||||
2. `micro agent demo` — print the provider-free first-agent demo command and next docs steps from the installed CLI.
|
||||
3. `micro agent quickcheck` (or `micro agent debug`) — when scaffold → run → chat → inspect stalls, print the short recovery map before you dive into the full debugging guide.
|
||||
4. `micro examples` — print the maintained provider-free runnable examples in copy/paste order.
|
||||
5. `micro zero-to-hero` — print the maintained one-command no-secret lifecycle harness and runnable examples.
|
||||
6. [Examples wayfinding index](examples/INDEX.md) — choose the smallest no-secret first-agent, maintained [0→hero support reference](examples/support/), and next interop examples from one map.
|
||||
7. [Smallest first-agent example](examples/first-agent/) — run one service-backed agent with a mock model and no provider key.
|
||||
8. [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.
|
||||
9. [Your First Agent](internal/website/docs/guides/your-first-agent.md) — build a
|
||||
service-backed agent and talk to it with `micro chat`.
|
||||
10. [Debugging your agent](internal/website/docs/guides/debugging-agents.md) — use
|
||||
`micro agent preflight` before `micro run`, `micro agent doctor` after `micro run`,
|
||||
then `micro chat` and `micro inspect agent <name>` to recover run history, memory,
|
||||
and provider checks when the first conversation does something unexpected.
|
||||
11. [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.
|
||||
2. `micro agent demo` — 从已安装的 CLI 打印无需提供商的首个智能体演示命令及后续文档步骤。
|
||||
3. `micro agent quickcheck`(或 `micro agent debug`)— 当 搭建 → 运行 → 聊天 → 检查 流程卡住时,在深入完整调试指南前,先打印简短恢复路线图。
|
||||
4. `micro examples` — 按可复制/粘贴顺序打印维护中的、无需提供商的可运行示例。
|
||||
5. `micro zero-to-hero` — 打印维护中的一条命令无密钥生命周期 harness 与可运行示例。
|
||||
6. [示例导航索引](examples/INDEX.md) — 从一张地图中选择最小的无密钥首个智能体示例、维护中的 [0→hero 支持参考](examples/support/),以及后续互操作示例。
|
||||
7. [最小首个智能体示例](examples/first-agent/) — 使用 mock 模型且无需提供商密钥,运行一个由服务支撑的智能体。
|
||||
8. [无密钥首个智能体脚本](internal/website/docs/guides/no-secret-first-agent.md) — 使用 mock 模型运行维护中的支持智能体,在无密钥情况下见证 服务 → 智能体 → 工作流 成功。
|
||||
9. [你的第一个智能体](internal/website/docs/guides/your-first-agent.md) — 构建一个由服务支撑的智能体,并通过 `micro chat` 与之对话。
|
||||
10. [调试你的智能体](internal/website/docs/guides/debugging-agents.md) — 在 `micro run` 之前使用 `micro agent preflight`,在 `micro run` 之后使用 `micro agent doctor`,
|
||||
然后使用 `micro chat` 和 `micro inspect agent <name>` 恢复运行历史、记忆
|
||||
以及提供商检查,以应对首次对话出现意外行为的情况。
|
||||
11. [0→hero 参考](internal/website/docs/guides/zero-to-hero.md) — 通过与服务维护 harness 一致的 搭建、运行、聊天、检查、flow
|
||||
历史与部署 dry-run 命令,完成 服务 → 智能体 → 工作流 循环。
|
||||
|
||||
### Autonomous improvement loop
|
||||
### 自主改进循环
|
||||
|
||||
Want the same services → agents → workflows lifecycle applied to your
|
||||
repository? `micro loop` scaffolds the autonomous improvement loop used by Go
|
||||
Micro itself: a North Star, ranked issue queue, role prompts, GitHub Actions
|
||||
workflows, and verification for CI-gated PRs.
|
||||
想将同样的 服务 → 智能体 → 工作流 生命周期应用到你的仓库?`micro loop` 会搭建 Go
|
||||
Micro 自身使用的自主改进循环:North Star、排序后的问题队列、角色提示词、GitHub Actions
|
||||
工作流,以及面向 CI 门禁 PR 的验证。
|
||||
|
||||
```bash
|
||||
micro loop init --roles all
|
||||
micro loop verify
|
||||
```
|
||||
|
||||
Before turning on the schedule, configure a dispatch token such as
|
||||
`CODEX_TRIGGER_TOKEN`, protect the default branch with required CI checks
|
||||
(`go build ./...`, `go test ./...`, and `golangci-lint run ./...` for this
|
||||
repository), and seed `.github/loop/PRIORITIES.md` with one scoped issue per
|
||||
increment. See the [`micro loop` quickstart](internal/website/docs/guides/micro-loop.md)
|
||||
for the setup checklist and operating model.
|
||||
在启用定时任务之前,请配置 dispatch token,例如
|
||||
`CODEX_TRIGGER_TOKEN`,用必需的 CI 检查保护默认分支
|
||||
(对本仓库为 `go build ./...`、`go test ./...` 和 `golangci-lint run ./...`),
|
||||
并在每个增量中为 `.github/loop/PRIORITIES.md` 填入一个范围明确的问题。设置清单与运行模型请参阅 [`micro loop` 快速开始](internal/website/docs/guides/micro-loop.md)。
|
||||
|
||||
### Generate from a prompt — with an LLM key
|
||||
### 通过提示词生成 — 需要 LLM key
|
||||
|
||||
Set a provider key, describe what you want, and the AI designs services, writes handlers, compiles, and starts them:
|
||||
设置提供商密钥,描述你的需求,AI 会设计服务、编写 handler、编译并启动它们:
|
||||
|
||||
```bash
|
||||
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
||||
micro run --prompt "a task management system with categories" --provider anthropic
|
||||
```
|
||||
|
||||
The AI designs the architecture, you review it, then it generates handlers with real business logic, compiles them, and starts them:
|
||||
AI 设计架构,你进行评审,随后它会生成包含真实业务逻辑的 handler、编译并启动:
|
||||
|
||||
```
|
||||
Services:
|
||||
@@ -168,7 +164,7 @@ Micro
|
||||
◆ agent
|
||||
```
|
||||
|
||||
Then talk to your services from the console:
|
||||
然后在控制台中与你的服务对话:
|
||||
|
||||
```
|
||||
> Create a project called Launch, then add three tasks to it
|
||||
@@ -182,7 +178,7 @@ Then talk to your services from the console:
|
||||
Created project Launch and added three tasks to it.
|
||||
```
|
||||
|
||||
When you need a capability that doesn't exist, the agent generates a new service mid-conversation:
|
||||
当你需要尚不存在的能力时,代理会在对话过程中生成一项新服务:
|
||||
|
||||
```
|
||||
> I need to track shipping. Create a shipment for order 123 to London.
|
||||
@@ -195,25 +191,25 @@ When you need a capability that doesn't exist, the agent generates a new service
|
||||
Created shipment for order 123 going to London.
|
||||
```
|
||||
|
||||
Edit the generated code by hand at any time — re-running preserves your changes. [Read more](https://go-micro.dev/blog/13).
|
||||
你可以随时手动编辑生成的代码——重新运行会保留你的修改。[了解更多](https://go-micro.dev/blog/13).
|
||||
|
||||
## Why an Agent Harness
|
||||
## 为何需要 Agent Harness
|
||||
|
||||
The first wave of agent frameworks helped developers put a model in a loop. The next problem is operating that loop: connecting it to real tools, scoping what it can touch, preserving state, routing work to specialists, recovering from failures, observing what happened, and letting other agents call it. That is harness work.
|
||||
第一波代理框架帮助开发者让模型进入循环。下一个问题是如何运营这个循环:将其连接到真实工具、限定可触及范围、保留状态、将工作路由给专家、从故障中恢复、观测发生了什么,并让其他代理调用它。这就是 harness 工作。
|
||||
|
||||
Go Micro's answer is to make the harness the same thing you already deploy:
|
||||
Go Micro 的答案是:让 harness 与你已部署的东西合二为一:
|
||||
|
||||
- **Tools are services** — endpoint metadata becomes tool schema; RPC executes the call.
|
||||
- **Agents are services** — they register, discover, load-balance, and expose `Agent.Chat`.
|
||||
- **Workflows are durable code paths** — use flows when the path is known; dispatch to agents when it is not.
|
||||
- **Safety lives at execution** — `MaxSteps`, `LoopLimit`, `ApproveTool`, and tool wrappers run where actions happen.
|
||||
- **Interop is built in** — MCP for tools, A2A for agents, x402 for paid tools.
|
||||
- **工具即服务** — 端点元数据成为工具 schema;RPC 执行调用。
|
||||
- **代理即服务** — 它们注册、发现、负载均衡,并暴露 `Agent.Chat`。
|
||||
- **工作流即持久化代码路径** — 路径已知时使用 flows;未知时分派给代理。
|
||||
- **安全落在执行层** — `MaxSteps`、`LoopLimit`、`ApproveTool` 以及工具包装器在动作发生处运行。
|
||||
- **互操作内置** — 工具用 MCP,代理用 A2A,付费工具用 x402。
|
||||
|
||||
Use Go Micro when the agent has to operate a system, not just answer a prompt.
|
||||
当代理需要操作系统而不仅是回答提示时,使用 Go Micro。
|
||||
|
||||
## Writing Services
|
||||
## 编写服务
|
||||
|
||||
Under the hood, a service is a struct with methods. Doc comments and `@example` tags become tool descriptions for AI agents automatically.
|
||||
底层而言,服务是带方法的 struct。文档注释和 `@example` 标签会自动成为 AI 代理的工具描述。
|
||||
|
||||
```go
|
||||
package main
|
||||
@@ -248,7 +244,7 @@ func main() {
|
||||
}
|
||||
```
|
||||
|
||||
Run it and everything is accessible — REST, gRPC, MCP, agent playground:
|
||||
运行后一切皆可访问——REST、gRPC、MCP、agent playground:
|
||||
|
||||
```bash
|
||||
micro run
|
||||
@@ -258,16 +254,16 @@ micro run
|
||||
# MCP Tools: http://localhost:8080/mcp/tools
|
||||
```
|
||||
|
||||
You can also scaffold a service from a template:
|
||||
你也可以从模板脚手架生成服务:
|
||||
|
||||
```bash
|
||||
micro new helloworld
|
||||
micro new contacts --template crud
|
||||
```
|
||||
|
||||
## Building Agents
|
||||
## 构建代理
|
||||
|
||||
An Agent is a service with an LLM inside it. It has a proto-defined `Agent.Chat` RPC endpoint, registers in the registry, and is callable like any service:
|
||||
代理是内置 LLM 的服务。它拥有 proto 定义的 `Agent.Chat` RPC 端点,在 registry 中注册,并可像任何服务一样被调用:
|
||||
|
||||
```go
|
||||
agent := micro.NewAgent("task-mgr",
|
||||
@@ -278,7 +274,7 @@ agent := micro.NewAgent("task-mgr",
|
||||
agent.Run()
|
||||
```
|
||||
|
||||
The agent discovers its services from the registry, scopes its tools to their endpoints, and maintains conversation memory in the store. It registers itself so `micro chat` and other agents can find it.
|
||||
代理从 registry 发现其服务,将工具限定到对应端点,并在 store 中维护对话记忆。它会自我注册,以便 `micro chat` 及其他代理能找到它。
|
||||
|
||||
```go
|
||||
// Programmatic interaction
|
||||
@@ -286,21 +282,21 @@ resp, _ := agent.Ask(ctx, "What tasks are overdue?")
|
||||
fmt.Println(resp.Reply)
|
||||
```
|
||||
|
||||
Multiple agents coordinate via RPC — each is a service with an `Agent.Chat` endpoint. `micro chat` routes to the right one.
|
||||
多个代理通过 RPC 协调——每个都是带有 `Agent.Chat` 端点的服务。`micro chat` 会路由到正确的那一个。
|
||||
|
||||
```bash
|
||||
micro agent list # list registered agents
|
||||
micro call task-mgr Agent.Chat '{"message": "What tasks are overdue?"}'
|
||||
```
|
||||
|
||||
### Plan & Delegate
|
||||
### 规划与委托(Plan & Delegate)
|
||||
|
||||
Every agent gets two built-in harness capabilities, exposed as tools — no extra setup or separate graph runtime:
|
||||
每个代理都具备两种内置 harness 能力,以工具形式暴露——无需额外设置或独立的图运行时:
|
||||
|
||||
- **`plan`** — for multi-step work, the agent records an ordered plan in its store-backed memory and stays oriented across turns.
|
||||
- **`delegate`** — the agent hands a self-contained subtask to another agent. If a registered agent already owns the relevant services, the hand-off goes over RPC to that agent; otherwise a focused, short-lived sub-agent is created for the subtask with its own isolated context.
|
||||
- **`plan`** — 对于多步骤工作,代理在其 store 支撑的记忆中记录有序计划,并在多轮对话中保持方向。
|
||||
- **`delegate`** — 代理将自包含的子任务交给另一个代理。若已注册的代理已拥有相关服务,则通过 RPC 交接给该代理;否则会为该子任务创建一个专注、短命的子代理,并配备独立隔离的上下文。
|
||||
|
||||
This keeps intelligence distributed: an agent doesn't need to know *how* to do everything, only *who* does. See [examples/agent-plan-delegate](examples/agent-plan-delegate/).
|
||||
这让智能保持分布式:代理不必知道*如何*完成一切,只需知道*谁*来做。参见 [examples/agent-plan-delegate](examples/agent-plan-delegate/)。
|
||||
|
||||
```go
|
||||
// A sub-agent is just an agent — created with New, talked to with Ask.
|
||||
@@ -308,9 +304,9 @@ This keeps intelligence distributed: an agent doesn't need to know *how* to do e
|
||||
resp, _ := agent.Ask(ctx, "Plan the launch, create the tasks, and have comms notify the owner.")
|
||||
```
|
||||
|
||||
### Batteries included, pluggable
|
||||
### 开箱即用,可插拔
|
||||
|
||||
Just as a service composes pluggable abstractions (registry, broker, store), an agent composes a **model**, **memory**, and **tools** — sane defaults out of the box, each swappable.
|
||||
正如服务组合可插拔抽象(registry、broker、store),代理组合 **model**、**memory** 和 **tools**——开箱即用的合理默认,每一项都可替换。
|
||||
|
||||
```go
|
||||
agent := micro.NewAgent("assistant",
|
||||
@@ -325,11 +321,11 @@ agent := micro.NewAgent("assistant",
|
||||
)
|
||||
```
|
||||
|
||||
**Memory** is durable and store-backed by default (Postgres, NATS KV, or file), so an agent picks up where it left off after a restart — or supply your own with `AgentMemory`. Long-running agents can opt into `AgentCompactMemory(maxMessages, keepRecent)`: older turns are collapsed into a deterministic summary, recent turns stay verbatim, and relevant archived turns are recalled on future asks without replaying the whole conversation. **Tools** are your services automatically, plus any function you register with `AgentTool`.
|
||||
**Memory(记忆)** 默认持久且由 store 支撑(Postgres、NATS KV 或 file),因此代理在重启后可从上次停下的地方继续——也可用 `AgentMemory` 提供你自己的实现。长期运行的代理可选用 `AgentCompactMemory(maxMessages, keepRecent)`:较早轮次被折叠为确定性摘要,最近轮次保持原文,相关归档轮次在未来请求时被召回,而无需重放整段对话。**Tools(工具)** 自动就是你的服务,加上你用 `AgentTool` 注册的任何函数。
|
||||
|
||||
### Paid tools (x402)
|
||||
### 付费工具(x402)
|
||||
|
||||
Every endpoint is an AI-callable tool — and it can be a *paid* tool. Go Micro supports [x402](https://x402.org), the HTTP 402 payment standard for agents, so a tool can require a stablecoin payment and an agent can settle it autonomously. It's opt-in and carries no crypto in the framework: verification is delegated to a pluggable facilitator (Coinbase, Alchemy, self-hosted), so Base and Solana are just different facilitators.
|
||||
每个端点都是 AI 可调用的工具——而且可以是*付费*工具。Go Micro 支持 [x402](https://x402.org), 面向代理的 HTTP 402 支付标准,因此工具可要求稳定币支付,代理可自主结算。这是可选功能,框架本身不携带加密货币逻辑:验证委托给可插拔的 facilitator(Coinbase、Alchemy、自托管),因此 Base 和 Solana 只是不同的 facilitator。
|
||||
|
||||
```bash
|
||||
# Charge for tool calls at the MCP gateway (off unless you set a pay-to address)
|
||||
@@ -338,84 +334,84 @@ micro mcp serve --x402_pay_to 0xYourAddress --x402_network solana --x402_amount
|
||||
micro mcp serve --x402_config x402.json
|
||||
```
|
||||
|
||||
See the [Payments (x402) guide](internal/website/docs/guides/x402-payments.md).
|
||||
参见 [Payments (x402) guide](internal/website/docs/guides/x402-payments.md)。
|
||||
|
||||
### Reachable by other agents (A2A)
|
||||
### 可被其他代理访问(A2A)
|
||||
|
||||
Within a Go Micro system, agents reach each other over RPC. To make them reachable by agents on *other* frameworks, Go Micro speaks the [Agent2Agent (A2A) protocol](https://a2a-protocol.org). The A2A gateway discovers your agents from the registry, generates an Agent Card for each from its metadata — the same way the MCP gateway derives tools from service endpoints — and translates incoming A2A tasks to the agent's `Agent.Chat` RPC. No per-agent code: register an agent and it's reachable over A2A.
|
||||
在 Go Micro 系统内,代理通过 RPC 相互访问。要让*其他*框架上的代理也能访问它们,Go Micro 使用 [Agent2Agent (A2A) protocol](https://a2a-protocol.org). A2A 网关从 registry 发现你的代理,根据其元数据为每个代理生成 Agent Card——与 MCP 网关从服务端点派生工具的方式相同——并将传入的 A2A 任务翻译为代理的 `Agent.Chat` RPC。无需为每个代理编写代码:注册代理即可通过 A2A 访问。
|
||||
|
||||
```bash
|
||||
micro a2a serve --address :4000 # gateway: expose every registered agent over A2A
|
||||
micro a2a list # agents and their Agent Card URLs
|
||||
```
|
||||
|
||||
Or skip the gateway entirely — an agent can serve its own A2A endpoint directly, handling tasks in-process:
|
||||
或者完全跳过网关——代理可直接提供自己的 A2A 端点,在进程内处理任务:
|
||||
|
||||
```go
|
||||
micro.NewAgent("task-mgr", micro.AgentServices("task"), micro.AgentA2A(":4000"))
|
||||
```
|
||||
|
||||
It works both ways. To call an agent on another framework, an `a2a.Client` is wired into the two places that hand off work: `flow.A2A(url)` as a workflow step (the cross-framework `Dispatch`), and `delegate` to an `http(s)` URL from inside an agent.
|
||||
双向皆可。要调用其他框架上的代理,`a2a.Client` 会接入两处交接工作的地方:作为工作流步骤的 `flow.A2A(url)`(跨框架的 `Dispatch`),以及从代理内部向 `http(s)` URL 发起的 `delegate`。
|
||||
|
||||
MCP exposes your services as tools; A2A exposes your agents as agents. See the [A2A guide](internal/website/docs/guides/a2a-protocol.md).
|
||||
MCP 将你的服务暴露为工具;A2A 将你的代理暴露为代理。参见 [A2A guide](internal/website/docs/guides/a2a-protocol.md)。
|
||||
|
||||
## Features
|
||||
## 功能特性
|
||||
|
||||
### AI
|
||||
|
||||
| Feature | Details |
|
||||
| 功能 | 详情 |
|
||||
|---------|---------|
|
||||
| Agents | `micro.NewAgent()` — intelligent layer that manages services |
|
||||
| Plan & delegate | Built-in agent tools — plan multi-step work, delegate subtasks to other agents |
|
||||
| Pluggable memory | Durable store-backed conversation memory by default; swap with `AgentMemory` |
|
||||
| Custom tools | `AgentTool` — give an agent any function as a tool, beyond its services |
|
||||
| Guardrails | `MaxSteps` (stop on count), `LoopLimit` (stop repeated no-progress calls), `ApproveTool` (human-in-the-loop) |
|
||||
| Tool middleware | `AgentWrapTool` — wrap tool execution for logging, metrics, or retries (like client/server wrappers) |
|
||||
| Workflows | `micro.NewFlow()` — event-driven; one step, ordered durable steps, or triggers an agent |
|
||||
| Durable execution | Checkpointed flow steps survive a crash and resume where they stopped; store-backed by default, pluggable backend |
|
||||
| MCP gateway | Every endpoint is an AI tool automatically |
|
||||
| A2A gateway | Every agent is reachable over the Agent2Agent protocol; cards generated from the registry (`micro a2a`) |
|
||||
| Payments (x402) | Opt-in per-call payments for tools via the x402 standard; pluggable facilitator (Base, Solana, …) |
|
||||
| 9 LLM providers | Anthropic, OpenAI, Gemini, Groq, Mistral, Together, Atlas Cloud, MiniMax, Ollama (local + cloud) |
|
||||
| Interactive console | `micro run` includes a chat console for talking to services |
|
||||
| Service generation | `micro run --prompt` — describe a system, get running services |
|
||||
| Agents(智能体) | `micro.NewAgent()` — 管理服务的智能层 |
|
||||
| Plan & delegate(规划与委派) | 内置 agent 工具 — 规划多步骤工作,将子任务委派给其他 agent |
|
||||
| Pluggable memory(可插拔记忆) | 默认基于持久化存储的对话记忆;可通过 `AgentMemory` 替换 |
|
||||
| Custom tools(自定义工具) | `AgentTool` — 为 agent 提供除服务之外的任意函数作为工具 |
|
||||
| Guardrails(护栏) | `MaxSteps`(按次数停止)、`LoopLimit`(停止重复的无进展调用)、`ApproveTool`(human-in-the-loop,人在回路) |
|
||||
| Tool middleware(工具中间件) | `AgentWrapTool` — 包装工具执行以实现日志、指标或重试(类似 client/server wrappers) |
|
||||
| Workflows(工作流) | `micro.NewFlow()` — 事件驱动;单步、有序持久化步骤,或触发 agent |
|
||||
| Durable execution(持久化执行) | 带检查点的流程步骤可在崩溃后存活并从停止处恢复;默认基于存储,后端可插拔 |
|
||||
| MCP gateway | 每个 endpoint 自动成为 AI 工具 |
|
||||
| A2A gateway | 每个 agent 均可通过 Agent2Agent 协议访问;卡片由 registry(`micro a2a`)生成 |
|
||||
| Payments (x402) | 通过 x402 标准按调用选择性为工具付费;facilitator 可插拔(Base、Solana 等) |
|
||||
| 9 个 LLM 提供商 | Anthropic、OpenAI、Gemini、Groq、Mistral、Together、Atlas Cloud、MiniMax、Ollama(本地 + 云端) |
|
||||
| Interactive console(交互式控制台) | `micro run` 包含与服务对话的聊天控制台 |
|
||||
| Service generation(服务生成) | `micro run --prompt` — 描述系统即可获得运行中的服务 |
|
||||
|
||||
### Framework
|
||||
### 框架
|
||||
|
||||
| Feature | Details |
|
||||
| 功能 | 详情 |
|
||||
|---------|---------|
|
||||
| Service registry | mDNS (default), Consul, etcd |
|
||||
| RPC client/server | gRPC transport, load balancing, streaming |
|
||||
| Pub/sub events | NATS, RabbitMQ, HTTP broker |
|
||||
| Key-value store | File (bbolt), Postgres, NATS KV |
|
||||
| Typed model layer | CRUD + queries, SQLite/Postgres backends |
|
||||
| Everything swappable | All abstractions are Go interfaces |
|
||||
| Service registry(服务注册) | mDNS(默认)、Consul、etcd |
|
||||
| RPC client/server | gRPC 传输、负载均衡、流式传输 |
|
||||
| Pub/sub events(发布/订阅事件) | NATS、RabbitMQ、HTTP broker |
|
||||
| Key-value store(键值存储) | File (bbolt)、Postgres、NATS KV |
|
||||
| Typed model layer(类型化模型层) | CRUD + 查询,SQLite/Postgres 后端 |
|
||||
| Everything swappable(一切皆可替换) | 所有抽象均为 Go interface |
|
||||
|
||||
### Developer experience & deployment
|
||||
### 开发者体验与部署
|
||||
|
||||
| Feature | Details |
|
||||
| 功能 | 详情 |
|
||||
|---------|---------|
|
||||
| Hot reload | `micro run` watches files, rebuilds on change |
|
||||
| Templates | `micro new --template crud/pubsub/api` |
|
||||
| One-command deploy | `micro deploy user@server` — SSH + systemd, no Docker |
|
||||
| Hot reload(热重载) | `micro run` 监听文件变更并重新构建 |
|
||||
| Templates(模板) | `micro new --template crud/pubsub/api` |
|
||||
| One-command deploy(一键部署) | `micro deploy user@server` — SSH + systemd,无需 Docker |
|
||||
|
||||
## CLI
|
||||
|
||||
| Command | Purpose |
|
||||
| 命令 | 用途 |
|
||||
|---------|---------|
|
||||
| `micro run --prompt "..."` | Generate services + agent, start with interactive console |
|
||||
| `micro run` | Dev mode: hot reload, gateway, interactive console |
|
||||
| `micro run -d` | Detached mode (no console) |
|
||||
| `micro chat` | Standalone chat (when not using micro run) |
|
||||
| `micro agent list` | List registered agents |
|
||||
| `micro new myservice` | Scaffold a service |
|
||||
| `micro call service endpoint '{}'` | Call a service or agent from the CLI |
|
||||
| `micro build` | Compile production binaries |
|
||||
| `micro deploy user@server` | Deploy via SSH + systemd |
|
||||
| `micro run --prompt "..."` | 生成服务与 agent,并启动交互式控制台 |
|
||||
| `micro run` | 开发模式:热重载、gateway、交互式控制台 |
|
||||
| `micro run -d` | 分离模式(无控制台) |
|
||||
| `micro chat` | 独立聊天(未使用 micro run 时) |
|
||||
| `micro agent list` | 列出已注册的 agent |
|
||||
| `micro new myservice` | 脚手架生成服务 |
|
||||
| `micro call service endpoint '{}'` | 从 CLI 调用服务或 agent |
|
||||
| `micro build` | 编译生产二进制文件 |
|
||||
| `micro deploy user@server` | 通过 SSH + systemd 部署 |
|
||||
|
||||
## Multi-Service Projects
|
||||
## 多服务项目
|
||||
|
||||
Run multiple services together:
|
||||
同时运行多个服务:
|
||||
|
||||
```go
|
||||
users := micro.NewService("users", micro.Address(":9001"))
|
||||
@@ -428,7 +424,7 @@ g := micro.NewGroup(users, orders)
|
||||
g.Run()
|
||||
```
|
||||
|
||||
Or use a `micro.mu` config file:
|
||||
或使用 `micro.mu` 配置文件:
|
||||
|
||||
```
|
||||
service users
|
||||
@@ -439,9 +435,9 @@ service orders
|
||||
depends users
|
||||
```
|
||||
|
||||
## Data Model
|
||||
## 数据模型
|
||||
|
||||
Typed persistence with CRUD and queries:
|
||||
类型化持久化,支持 CRUD 与查询:
|
||||
|
||||
```go
|
||||
type User struct {
|
||||
@@ -458,13 +454,13 @@ var results []*User
|
||||
db.List(ctx, &results, model.Where("email", "alice@example.com"))
|
||||
```
|
||||
|
||||
Backends: memory (default), SQLite, Postgres.
|
||||
后端:memory(默认)、SQLite、Postgres。
|
||||
|
||||
## AI Providers
|
||||
## AI 提供商
|
||||
|
||||
Swap providers with a single import — same interface everywhere:
|
||||
单次 import 即可切换提供商 — 各处使用相同 interface:
|
||||
|
||||
| Provider | Default Model |
|
||||
| 提供商 | 默认模型 |
|
||||
|----------|---------------|
|
||||
| Anthropic | `claude-sonnet-4-20250514` |
|
||||
| OpenAI | `gpt-4o` |
|
||||
@@ -481,34 +477,34 @@ m := ai.New("anthropic", ai.WithAPIKey(key))
|
||||
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.
|
||||
初次接触 agent?请跟随 [first-agent on-ramp](#first-agent-on-ramp),然后使用 [examples index](examples/README.md) 查看完整的 services → agents → workflows 映射。
|
||||
|
||||
- [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
|
||||
- [hello-world](examples/hello-world/) — 基础 RPC 服务
|
||||
- [multi-service](examples/multi-service/) — 单个二进制文件中运行多个服务
|
||||
- [mcp](examples/mcp/) — 与 AI agent 的 MCP 集成
|
||||
- [first-agent](examples/first-agent/) — 最小化、无需 provider 的服务支撑型 agent
|
||||
- [agent-plan-delegate](examples/agent-plan-delegate/) — Agent 规划与多 agent 委派
|
||||
- [agent-durable](examples/agent-durable/) — 检查点与恢复 agent 运行,无需重放已完成的工具副作用
|
||||
- [grpc-interop](examples/grpc-interop/) — 从任意 gRPC 客户端调用 go-micro
|
||||
|
||||
See [all examples](examples/README.md).
|
||||
查看 [all examples](examples/README.md)。
|
||||
|
||||
## Docs
|
||||
## 文档
|
||||
|
||||
- [Getting Started](internal/website/docs/getting-started.md)
|
||||
- [AI Integration](internal/website/docs/ai-integration.md)
|
||||
- [Your First Agent](internal/website/docs/guides/your-first-agent.md)
|
||||
- [0→hero Reference](internal/website/docs/guides/zero-to-hero.md)
|
||||
- [Agents and Workflows](internal/website/docs/guides/agents-and-workflows.md)
|
||||
- [Agent Design](internal/docs/AGENT_DESIGN.md)
|
||||
- [Plan & Delegate](internal/website/docs/guides/plan-delegate.md)
|
||||
- [Agent Guardrails](internal/website/docs/guides/agent-guardrails.md)
|
||||
- [Payments (x402)](internal/website/docs/guides/x402-payments.md)
|
||||
- [MCP & AI Agents](internal/website/docs/mcp.md)
|
||||
- [Data Model](internal/website/docs/model.md)
|
||||
- [Deployment](internal/website/docs/deployment.md)
|
||||
- [Plugins](internal/website/docs/plugins.md)
|
||||
- [入门指南](internal/website/docs/getting-started.md)
|
||||
- [AI 集成](internal/website/docs/ai-integration.md)
|
||||
- [你的第一个 Agent](internal/website/docs/guides/your-first-agent.md)
|
||||
- [0→hero 参考](internal/website/docs/guides/zero-to-hero.md)
|
||||
- [Agent 与工作流](internal/website/docs/guides/agents-and-workflows.md)
|
||||
- [Agent 设计](internal/docs/AGENT_DESIGN.md)
|
||||
- [规划与委派](internal/website/docs/guides/plan-delegate.md)
|
||||
- [Agent 护栏](internal/website/docs/guides/agent-guardrails.md)
|
||||
- [支付(x402)](internal/website/docs/guides/x402-payments.md)
|
||||
- [MCP 与 AI Agent](internal/website/docs/mcp.md)
|
||||
- [数据模型](internal/website/docs/model.md)
|
||||
- [部署](internal/website/docs/deployment.md)
|
||||
- [插件](internal/website/docs/plugins.md)
|
||||
|
||||
Package reference: https://pkg.go.dev/go-micro.dev/v6
|
||||
包参考:https://pkg.go.dev/go-micro.dev/v6
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
# WeHub 来源说明
|
||||
|
||||
- 原始项目:`micro/go-micro`
|
||||
- 原始仓库:https://github.com/micro/go-micro
|
||||
- 导入方式:上游默认分支的最新快照
|
||||
- 原作者、版权和许可证信息以原始仓库及本仓库 LICENSE 为准
|
||||
- 本文件仅用于记录来源,不代表 WeHub 是原项目作者
|
||||
+1
-4
@@ -92,10 +92,7 @@ func (f *Flow) runStepSpan(ctx context.Context, step Step, in State) (State, int
|
||||
span.SetAttributes(attribute.String(AttrFlowVerificationStatus, "failed"))
|
||||
}
|
||||
}
|
||||
if a, ok := isAwaitInput(err); ok {
|
||||
// A suspend is normal control flow, not a step error.
|
||||
span.SetStatus(codes.Ok, "waiting: "+a.Key)
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
span.RecordError(err)
|
||||
span.SetAttributes(attribute.String(AttrFlowErrorKind, string(ai.ClassifyError(err))))
|
||||
span.SetStatus(codes.Error, err.Error())
|
||||
|
||||
+2
-136
@@ -4,7 +4,6 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"sort"
|
||||
"text/template"
|
||||
@@ -111,8 +110,7 @@ type Run struct {
|
||||
Flow string `json:"flow"`
|
||||
State State `json:"state"`
|
||||
Steps []StepRecord `json:"steps"`
|
||||
Status string `json:"status"` // running | waiting | done | failed
|
||||
Await *AwaitState `json:"await,omitempty"`
|
||||
Status string `json:"status"` // running | done | failed
|
||||
Started time.Time `json:"started"`
|
||||
Updated time.Time `json:"updated"`
|
||||
}
|
||||
@@ -338,54 +336,6 @@ func LLM(prompt string) StepFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// AwaitInput is the control signal a step returns (via Await) to suspend a run
|
||||
// pending external input. runFrom recognizes it, checkpoints the run as
|
||||
// "waiting", and returns cleanly — a suspend is not a failure. ResumeWith
|
||||
// injects the input and continues.
|
||||
type AwaitInput struct {
|
||||
Key string // labels what is awaited (e.g. "approval")
|
||||
Prompt string // human-facing description of the input needed
|
||||
}
|
||||
|
||||
func (e *AwaitInput) Error() string {
|
||||
if e.Prompt != "" {
|
||||
return fmt.Sprintf("flow: awaiting input %q: %s", e.Key, e.Prompt)
|
||||
}
|
||||
return fmt.Sprintf("flow: awaiting input %q", e.Key)
|
||||
}
|
||||
|
||||
// AwaitState records, on a suspended run, what it is waiting for.
|
||||
type AwaitState struct {
|
||||
Step string `json:"step"`
|
||||
Key string `json:"key"`
|
||||
Prompt string `json:"prompt,omitempty"`
|
||||
}
|
||||
|
||||
func isAwaitInput(err error) (*AwaitInput, bool) {
|
||||
var a *AwaitInput
|
||||
if errors.As(err, &a) {
|
||||
return a, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// Await is a StepFunc that suspends the run pending external input. The run is
|
||||
// checkpointed with status "waiting" and returned cleanly; a later call to
|
||||
// Flow.ResumeWith(ctx, runID, input) completes this step with the injected
|
||||
// input and continues to the next step. key labels what is awaited (surfaced on
|
||||
// the run and via Flow.Waiting); prompt describes the input needed.
|
||||
func Await(key, prompt string) StepFunc {
|
||||
return func(_ context.Context, in State) (State, error) {
|
||||
return in, &AwaitInput{Key: key, Prompt: prompt}
|
||||
}
|
||||
}
|
||||
|
||||
// AwaitStep is a convenience for a named await step:
|
||||
// Step{Name: name, Run: Await(key, prompt)}.
|
||||
func AwaitStep(name, key, prompt string) Step {
|
||||
return Step{Name: name, Run: Await(key, prompt)}
|
||||
}
|
||||
|
||||
// startRun begins a fresh run of the flow's steps with the given input.
|
||||
func (f *Flow) startRun(ctx context.Context, data string) (Run, error) {
|
||||
if err := validateSteps(f.opts.Steps); err != nil {
|
||||
@@ -471,79 +421,13 @@ func (f *Flow) Pending(ctx context.Context) ([]Run, error) {
|
||||
}
|
||||
var out []Run
|
||||
for _, r := range all {
|
||||
// Waiting runs need injected input (ResumeWith), not a restart, so a
|
||||
// recovery loop (ResumePending) should not pick them up.
|
||||
if r.Flow == f.name && r.Status != "done" && r.Status != "waiting" {
|
||||
if r.Flow == f.name && r.Status != "done" {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// Waiting returns this flow's runs suspended awaiting external input, each with
|
||||
// its Await metadata, so a caller can prompt for and inject the needed input
|
||||
// with ResumeWith.
|
||||
func (f *Flow) Waiting(ctx context.Context) ([]Run, error) {
|
||||
if f.checkpoint == nil {
|
||||
return nil, nil
|
||||
}
|
||||
all, err := f.checkpoint.List(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var out []Run
|
||||
for _, r := range all {
|
||||
if r.Flow == f.name && r.Status == "waiting" {
|
||||
out = append(out, r)
|
||||
}
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
// ResumeWith completes a suspended (waiting) run: it injects input for the
|
||||
// awaited step — the input becomes that step's output state — and continues
|
||||
// from the next step. It errors if the run is not waiting for input.
|
||||
func (f *Flow) ResumeWith(ctx context.Context, runID, input string) error {
|
||||
ctx, cancel := f.withTimeout(ctx)
|
||||
defer cancel()
|
||||
|
||||
if err := validateSteps(f.opts.Steps); err != nil {
|
||||
return err
|
||||
}
|
||||
if f.checkpoint == nil {
|
||||
return fmt.Errorf("flow %s has no checkpoint configured", f.name)
|
||||
}
|
||||
run, ok, err := f.checkpoint.Load(ctx, runID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !ok {
|
||||
return fmt.Errorf("run %s not found", runID)
|
||||
}
|
||||
if run.Status != "waiting" {
|
||||
return fmt.Errorf("run %s is not waiting for input (status %q)", runID, run.Status)
|
||||
}
|
||||
steps := f.opts.Steps
|
||||
i := stepIndex(steps, run.State.Stage)
|
||||
if i < 0 {
|
||||
return fmt.Errorf("run %s is waiting at unknown step %q", runID, run.State.Stage)
|
||||
}
|
||||
// The awaited step is satisfied by the injected input; record it done and
|
||||
// advance so runFrom re-enters at the next step.
|
||||
run.Steps[i].Status = "done"
|
||||
run.Steps[i].Result = truncate(input, 200)
|
||||
run.State.Data = []byte(input)
|
||||
if i+1 < len(steps) {
|
||||
run.State.Stage = steps[i+1].Name
|
||||
} else {
|
||||
run.State.Stage = ""
|
||||
}
|
||||
run.Await = nil
|
||||
run.Status = "running"
|
||||
_, err = f.runFrom(ctx, run)
|
||||
return err
|
||||
}
|
||||
|
||||
// runFrom executes steps from the run's current Stage to the end,
|
||||
// checkpointing before and after each step.
|
||||
func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
|
||||
@@ -580,19 +464,6 @@ func (f *Flow) runFrom(ctx context.Context, run Run) (Run, error) {
|
||||
out, attempts, verification, err := f.runStepSpan(ctx, step, run.State)
|
||||
run.Steps[i].Attempts = attempts
|
||||
applyVerificationRecord(&run.Steps[i], verification)
|
||||
if await, ok := isAwaitInput(err); ok {
|
||||
// Suspend the run pending external input — checkpoint and return
|
||||
// cleanly (not a failure). ResumeWith injects the input later.
|
||||
run.Steps[i].Status = "waiting"
|
||||
run.Status = "waiting"
|
||||
run.Await = &AwaitState{Step: step.Name, Key: await.Key, Prompt: await.Prompt}
|
||||
if saveErr := f.save(ctx, run); saveErr != nil {
|
||||
spanErr = saveErr
|
||||
return run, saveErr
|
||||
}
|
||||
f.log.Logf(logger.InfoLevel, "Flow %s run %s waiting for input %q at step %q", f.name, run.ID, await.Key, step.Name)
|
||||
return run, nil
|
||||
}
|
||||
if err != nil {
|
||||
spanErr = err
|
||||
run.Steps[i].Status = "failed"
|
||||
@@ -666,11 +537,6 @@ func (f *Flow) runStep(ctx context.Context, step Step, in State) (State, int, Ve
|
||||
attemptCtx = ai.WithRunInfo(ctx, info)
|
||||
}
|
||||
out, err := step.Run(attemptCtx, in)
|
||||
// An await signal is control flow, not a failure: suspend immediately
|
||||
// without retrying or grading.
|
||||
if _, ok := isAwaitInput(err); ok {
|
||||
return in, attempt, lastVerification, err
|
||||
}
|
||||
if err == nil && step.Verify != nil {
|
||||
lastVerification, err = step.Verify(attemptCtx, out)
|
||||
if err == nil && !lastVerification.Passed {
|
||||
|
||||
@@ -87,93 +87,6 @@ func TestFlowCheckpointResume(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowAwaitAndResumeWith(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
var firstCalls int
|
||||
var secondInput string
|
||||
|
||||
steps := []Step{
|
||||
{Name: "first", Run: func(_ context.Context, in State) (State, error) {
|
||||
firstCalls++
|
||||
in.Data = []byte("first-done")
|
||||
return in, nil
|
||||
}},
|
||||
AwaitStep("approval", "approve", "Approve to continue?"),
|
||||
{Name: "second", Run: func(_ context.Context, in State) (State, error) {
|
||||
secondInput = in.String()
|
||||
in.Data = []byte("second-done")
|
||||
return in, nil
|
||||
}},
|
||||
}
|
||||
|
||||
f := New("hitl", WithCheckpoint(StoreCheckpoint(mem, "hitl")), Steps(steps...))
|
||||
|
||||
// Execute suspends at the await step — a clean return, not an error.
|
||||
if err := f.Execute(context.Background(), "start"); err != nil {
|
||||
t.Fatalf("Execute should suspend cleanly, got %v", err)
|
||||
}
|
||||
if firstCalls != 1 {
|
||||
t.Fatalf("first step calls = %d, want 1", firstCalls)
|
||||
}
|
||||
|
||||
// A waiting run is not pending (restart), it needs input.
|
||||
if pend, _ := f.Pending(context.Background()); len(pend) != 0 {
|
||||
t.Errorf("a waiting run must not be pending, got %d", len(pend))
|
||||
}
|
||||
waiting, err := f.Waiting(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(waiting) != 1 {
|
||||
t.Fatalf("waiting runs = %d, want 1", len(waiting))
|
||||
}
|
||||
w := waiting[0]
|
||||
if w.Status != "waiting" || w.Await == nil || w.Await.Key != "approve" ||
|
||||
w.Await.Prompt != "Approve to continue?" || w.Await.Step != "approval" {
|
||||
t.Fatalf("await metadata = %+v (status %q)", w.Await, w.Status)
|
||||
}
|
||||
if w.State.Stage != "approval" {
|
||||
t.Fatalf("waiting stage = %q, want approval", w.State.Stage)
|
||||
}
|
||||
|
||||
// Injecting input completes the awaited step and runs the rest.
|
||||
if err := f.ResumeWith(context.Background(), w.ID, "approved"); err != nil {
|
||||
t.Fatalf("ResumeWith: %v", err)
|
||||
}
|
||||
if firstCalls != 1 {
|
||||
t.Errorf("completed step re-ran on resume; first calls = %d", firstCalls)
|
||||
}
|
||||
if secondInput != "approved" {
|
||||
t.Errorf("second step input = %q, want the injected 'approved'", secondInput)
|
||||
}
|
||||
if wr, _ := f.Waiting(context.Background()); len(wr) != 0 {
|
||||
t.Errorf("no waiting runs after resume, got %d", len(wr))
|
||||
}
|
||||
runs, _ := StoreCheckpoint(mem, "hitl").List(context.Background())
|
||||
if len(runs) != 1 || runs[0].Status != "done" {
|
||||
t.Fatalf("run should be done after resume, got %+v", runs)
|
||||
}
|
||||
if runs[0].Await != nil {
|
||||
t.Errorf("await metadata should be cleared after resume, got %+v", runs[0].Await)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowResumeWithRejectsNonWaiting(t *testing.T) {
|
||||
mem := store.NewMemoryStore()
|
||||
f := New("hitl2", WithCheckpoint(StoreCheckpoint(mem, "hitl2")),
|
||||
Steps(Step{Name: "only", Run: func(_ context.Context, in State) (State, error) { return in, nil }}))
|
||||
if err := f.Execute(context.Background(), "x"); err != nil {
|
||||
t.Fatalf("Execute: %v", err)
|
||||
}
|
||||
runs, _ := StoreCheckpoint(mem, "hitl2").List(context.Background())
|
||||
if len(runs) != 1 {
|
||||
t.Fatalf("runs = %d", len(runs))
|
||||
}
|
||||
if err := f.ResumeWith(context.Background(), runs[0].ID, "input"); err == nil {
|
||||
t.Error("ResumeWith on a completed (non-waiting) run should error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFlowStepContextIncludesRunInfo(t *testing.T) {
|
||||
var got ai.RunInfo
|
||||
step := Step{Name: "inspect", Run: func(ctx context.Context, in State) (State, error) {
|
||||
|
||||
+5
-108
@@ -27,14 +27,12 @@ package a2a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
@@ -66,20 +64,6 @@ type Options struct {
|
||||
Client client.Client
|
||||
// Logger for startup/debug output (defaults to log.Default()).
|
||||
Logger *log.Logger
|
||||
// AllowPushURL authorizes an outbound push-notification callback URL
|
||||
// (tasks/pushNotificationConfig/set). Return a non-nil error to reject it.
|
||||
// When nil, a default SSRF-safe policy applies: only http/https URLs whose
|
||||
// host does not resolve to a loopback, private, link-local, or unspecified
|
||||
// address are allowed, and the connection is pinned to that check at dial
|
||||
// time (DNS-rebinding safe). Set this to permit a trusted in-cluster
|
||||
// receiver, or to narrow delivery to an allowlist.
|
||||
AllowPushURL func(*url.URL) error
|
||||
// AP2PublicKey, when set, verifies AP2 payment/checkout mandates carried on
|
||||
// incoming A2A messages against this Ed25519 key and records the outcome in
|
||||
// each task's ap2Verifications (signature + task/context binding). When
|
||||
// unset, mandates are carried through unverified. This is opt-in so the
|
||||
// default flow stays free of a payment trust decision.
|
||||
AP2PublicKey ed25519.PublicKey
|
||||
}
|
||||
|
||||
// Gateway serves the A2A protocol over HTTP for the registry's agents.
|
||||
@@ -103,20 +87,7 @@ func New(opts Options) *Gateway {
|
||||
opts.BaseURL = "http://localhost" + opts.Address
|
||||
}
|
||||
opts.BaseURL = strings.TrimRight(opts.BaseURL, "/")
|
||||
g := &Gateway{opts: opts, disp: newDispatcher()}
|
||||
if opts.AllowPushURL != nil {
|
||||
// Operator owns the trust decision: use their policy and skip the
|
||||
// built-in private-IP dial guard so trusted in-cluster hosts resolve.
|
||||
g.disp.allowPushURL = opts.AllowPushURL
|
||||
g.disp.guardPushDial = false
|
||||
}
|
||||
if len(opts.AP2PublicKey) > 0 {
|
||||
pub := opts.AP2PublicKey
|
||||
g.disp.ap2Verify = func(s AP2SignedMandate, task Task) AP2Verification {
|
||||
return VerifyAP2ForTask(s, pub, task, nil)
|
||||
}
|
||||
}
|
||||
return g
|
||||
return &Gateway{opts: opts, disp: newDispatcher()}
|
||||
}
|
||||
|
||||
// Invoke runs an agent for one message and returns its reply. It is the
|
||||
@@ -127,48 +98,12 @@ type Invoke func(ctx context.Context, text string) (string, error)
|
||||
// StreamInvoke runs an agent for one message and returns streaming output chunks.
|
||||
type StreamInvoke func(ctx context.Context, text string) (ai.Stream, error)
|
||||
|
||||
// AgentHandlerOption configures an embedded A2A agent handler.
|
||||
type AgentHandlerOption func(*dispatcher)
|
||||
|
||||
// WithPushURLPolicy sets the push-notification callback URL policy for an
|
||||
// embedded agent handler (the analog of Options.AllowPushURL on the gateway).
|
||||
// Return a non-nil error to reject a URL. Without it, the default SSRF-safe
|
||||
// policy applies. Supplying a policy also disables the built-in private-IP dial
|
||||
// guard, so a trusted in-cluster receiver resolves.
|
||||
func WithPushURLPolicy(allow func(*url.URL) error) AgentHandlerOption {
|
||||
return func(d *dispatcher) {
|
||||
if allow == nil {
|
||||
return
|
||||
}
|
||||
d.allowPushURL = allow
|
||||
d.guardPushDial = false
|
||||
}
|
||||
}
|
||||
|
||||
// WithAP2PublicKey verifies AP2 mandates carried on incoming messages against
|
||||
// pub (the embedded-handler analog of Options.AP2PublicKey), recording the
|
||||
// outcome in each task's ap2Verifications. Without it, mandates are carried
|
||||
// unverified.
|
||||
func WithAP2PublicKey(pub ed25519.PublicKey) AgentHandlerOption {
|
||||
return func(d *dispatcher) {
|
||||
if len(pub) == 0 {
|
||||
return
|
||||
}
|
||||
d.ap2Verify = func(s AP2SignedMandate, task Task) AP2Verification {
|
||||
return VerifyAP2ForTask(s, pub, task, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewAgentHandler returns an http.Handler that serves the A2A protocol
|
||||
// for a single agent: its Agent Card at / and /.well-known/agent.json,
|
||||
// and the JSON-RPC endpoint at /. invoke runs the agent. This is what an
|
||||
// agent embeds to speak A2A directly, without a separate gateway.
|
||||
func NewAgentHandler(card AgentCard, invoke Invoke, opts ...AgentHandlerOption) http.Handler {
|
||||
func NewAgentHandler(card AgentCard, invoke Invoke) http.Handler {
|
||||
d := newDispatcher()
|
||||
for _, o := range opts {
|
||||
o(d)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
card.URL = strings.TrimRight(card.URL, "/")
|
||||
serveCard := func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, card) }
|
||||
@@ -183,11 +118,8 @@ func NewAgentHandler(card AgentCard, invoke Invoke, opts ...AgentHandlerOption)
|
||||
|
||||
// NewAgentStreamHandler is like NewAgentHandler, but serves A2A message/stream
|
||||
// by forwarding model chunks as server-sent task updates when stream is non-nil.
|
||||
func NewAgentStreamHandler(card AgentCard, invoke Invoke, stream StreamInvoke, opts ...AgentHandlerOption) http.Handler {
|
||||
func NewAgentStreamHandler(card AgentCard, invoke Invoke, stream StreamInvoke) http.Handler {
|
||||
d := newDispatcher()
|
||||
for _, o := range opts {
|
||||
o(d)
|
||||
}
|
||||
mux := http.NewServeMux()
|
||||
card.URL = strings.TrimRight(card.URL, "/")
|
||||
serveCard := func(w http.ResponseWriter, _ *http.Request) { writeJSON(w, http.StatusOK, card) }
|
||||
@@ -578,26 +510,10 @@ type dispatcher struct {
|
||||
pushConfigs map[string]PushNotificationConfig
|
||||
watchers map[string]map[chan *Task]struct{}
|
||||
order []string // task ids in insertion order, for bounded eviction
|
||||
|
||||
// allowPushURL authorizes an outbound push-notification callback URL; nil
|
||||
// means the default SSRF-safe policy. guardPushDial applies the private-IP
|
||||
// dial guard (on unless an operator supplied a custom policy).
|
||||
allowPushURL func(*url.URL) error
|
||||
guardPushDial bool
|
||||
|
||||
// ap2Verify, when non-nil, verifies each AP2 mandate carried on a task and
|
||||
// records the result in the task's AP2Verifications. Nil = carry unverified.
|
||||
ap2Verify func(AP2SignedMandate, Task) AP2Verification
|
||||
}
|
||||
|
||||
func newDispatcher() *dispatcher {
|
||||
return &dispatcher{
|
||||
tasks: map[string]*Task{},
|
||||
pushConfigs: map[string]PushNotificationConfig{},
|
||||
watchers: map[string]map[chan *Task]struct{}{},
|
||||
allowPushURL: defaultPushURLPolicy,
|
||||
guardPushDial: true,
|
||||
}
|
||||
return &dispatcher{tasks: map[string]*Task{}, pushConfigs: map[string]PushNotificationConfig{}, watchers: map[string]map[chan *Task]struct{}{}}
|
||||
}
|
||||
|
||||
func (d *dispatcher) serve(w http.ResponseWriter, r *http.Request, invoke Invoke) {
|
||||
@@ -835,11 +751,6 @@ func (d *dispatcher) setPushConfig(w http.ResponseWriter, req rpcRequest) {
|
||||
writeRPC(w, req.ID, nil, &rpcError{Code: errInvalidParams, Message: "invalid params"})
|
||||
return
|
||||
}
|
||||
// Reject SSRF-unsafe callback targets before storing them.
|
||||
if err := d.checkPushURL(p.PushNotificationConfig.URL); err != nil {
|
||||
writeRPC(w, req.ID, nil, &rpcError{Code: errInvalidParams, Message: "push notification url not allowed"})
|
||||
return
|
||||
}
|
||||
d.mu.Lock()
|
||||
task := d.tasks[p.ID]
|
||||
if task != nil {
|
||||
@@ -895,15 +806,6 @@ func (g *Gateway) callAgent(ctx context.Context, name, message string) (string,
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func (d *dispatcher) store(t *Task) {
|
||||
// Verify any AP2 mandates carried on the task (opt-in) and surface the
|
||||
// outcome so a downstream paid path can trust — or reject — the mandate.
|
||||
if d.ap2Verify != nil && len(t.AP2Mandates) > 0 && len(t.AP2Verifications) == 0 {
|
||||
v := make([]AP2Verification, 0, len(t.AP2Mandates))
|
||||
for _, m := range t.AP2Mandates {
|
||||
v = append(v, d.ap2Verify(m, *t))
|
||||
}
|
||||
t.AP2Verifications = v
|
||||
}
|
||||
d.mu.Lock()
|
||||
_, exists := d.tasks[t.ID]
|
||||
d.tasks[t.ID] = t
|
||||
@@ -1017,11 +919,6 @@ func (d *dispatcher) deliverPush(taskID string, task *Task) {
|
||||
if !ok || cfg.URL == "" || task == nil {
|
||||
return
|
||||
}
|
||||
// Defense in depth: re-validate the callback URL at delivery time in case
|
||||
// the policy tightened or the config was set before it applied.
|
||||
if err := d.checkPushURL(cfg.URL); err != nil {
|
||||
return
|
||||
}
|
||||
body, err := json.Marshal(task)
|
||||
if err != nil {
|
||||
return
|
||||
@@ -1036,7 +933,7 @@ func (d *dispatcher) deliverPush(taskID string, task *Task) {
|
||||
if cfg.Token != "" {
|
||||
req.Header.Set("Authorization", "Bearer "+cfg.Token)
|
||||
}
|
||||
resp, err := d.pushClient().Do(req)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err == nil && resp.Body != nil {
|
||||
_ = resp.Body.Close()
|
||||
}
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -224,10 +223,6 @@ func TestMessageSendContinuesExistingTask(t *testing.T) {
|
||||
|
||||
func TestPushNotificationConfigDeliversTaskUpdates(t *testing.T) {
|
||||
d := newDispatcher()
|
||||
// The test receiver is a loopback httptest server; authorize it the way a
|
||||
// deployment would authorize a trusted in-cluster push receiver.
|
||||
d.allowPushURL = func(*url.URL) error { return nil }
|
||||
d.guardPushDial = false
|
||||
updates := make(chan Task, 2)
|
||||
push := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if got := r.Header.Get("Authorization"); got != "Bearer secret" {
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
package a2a
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/ed25519"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -53,83 +50,6 @@ func TestAP2PaymentMandateX402RailReference(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAP2GatewayVerifiesInboundPaymentMandate drives a real A2A message/send
|
||||
// carrying a signed x402 payment mandate through the gateway and asserts the
|
||||
// mandate is verified (and the x402 rail carried) into the task a paid path
|
||||
// consults — and that a tampered mandate is surfaced as unverified.
|
||||
func TestAP2GatewayVerifiesInboundPaymentMandate(t *testing.T) {
|
||||
pub, priv := testAP2Key(t)
|
||||
d := newDispatcher()
|
||||
d.ap2Verify = func(s AP2SignedMandate, task Task) AP2Verification {
|
||||
return VerifyAP2ForTask(s, pub, task, nil)
|
||||
}
|
||||
invoke := func(context.Context, string) (string, error) { return "fetched", nil }
|
||||
|
||||
send := func(t *testing.T, mandate AP2SignedMandate) Task {
|
||||
t.Helper()
|
||||
msg := AP2AttachMandate(
|
||||
Message{Role: "user", Kind: "message", MessageID: "m1", Parts: []Part{{Kind: "text", Text: "pay and fetch"}}},
|
||||
mandate,
|
||||
)
|
||||
params, err := json.Marshal(sendParams{Message: msg})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"message/send","params":%s}`, params)
|
||||
return rpcTaskFromBody(t, d, body, invoke)
|
||||
}
|
||||
|
||||
rail := X402AP2Rail("payreq_777")
|
||||
good, err := SignAP2Mandate(AP2Mandate{ID: "pay-1", Kind: AP2PaymentMandate, Rail: &rail, IssuedAt: time.Unix(1, 0).UTC()}, "k", priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
task := send(t, good)
|
||||
if len(task.AP2Verifications) != 1 || !task.AP2Verifications[0].Verified {
|
||||
t.Fatalf("inbound payment mandate not verified: %+v", task.AP2Verifications)
|
||||
}
|
||||
if task.AP2Verifications[0].Kind != string(AP2PaymentMandate) {
|
||||
t.Errorf("verification kind = %q, want payment", task.AP2Verifications[0].Kind)
|
||||
}
|
||||
if len(task.AP2Mandates) != 1 || task.AP2Mandates[0].Mandate.Rail == nil ||
|
||||
task.AP2Mandates[0].Mandate.Rail.Type != "x402" || task.AP2Mandates[0].Mandate.Rail.Reference != "payreq_777" {
|
||||
t.Fatalf("x402 settlement rail not carried onto task: %+v", task.AP2Mandates)
|
||||
}
|
||||
|
||||
tampered := good
|
||||
tampered.Mandate.Amount = "999.00"
|
||||
bad := send(t, tampered)
|
||||
if len(bad.AP2Verifications) != 1 || bad.AP2Verifications[0].Verified {
|
||||
t.Fatalf("tampered mandate should be unverified: %+v", bad.AP2Verifications)
|
||||
}
|
||||
if !strings.Contains(bad.AP2Verifications[0].Error, "signature") {
|
||||
t.Errorf("tampered verification error = %q, want signature failure", bad.AP2Verifications[0].Error)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAP2CarriedUnverifiedWithoutKey confirms the default (no configured key)
|
||||
// is unchanged: mandates are carried but not verified.
|
||||
func TestAP2CarriedUnverifiedWithoutKey(t *testing.T) {
|
||||
_, priv := testAP2Key(t)
|
||||
d := newDispatcher() // no ap2Verify configured
|
||||
rail := X402AP2Rail("payreq_1")
|
||||
signed, err := SignAP2Mandate(AP2Mandate{ID: "pay-1", Kind: AP2PaymentMandate, Rail: &rail, IssuedAt: time.Unix(1, 0).UTC()}, "k", priv)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
msg := AP2AttachMandate(Message{Role: "user", Kind: "message", MessageID: "m1", Parts: []Part{{Kind: "text", Text: "x"}}}, signed)
|
||||
params, _ := json.Marshal(sendParams{Message: msg})
|
||||
body := fmt.Sprintf(`{"jsonrpc":"2.0","id":1,"method":"message/send","params":%s}`, params)
|
||||
task := rpcTaskFromBody(t, d, body, func(context.Context, string) (string, error) { return "ok", nil })
|
||||
if len(task.AP2Mandates) != 1 {
|
||||
t.Fatalf("mandate should still be carried: %+v", task.AP2Mandates)
|
||||
}
|
||||
if len(task.AP2Verifications) != 0 {
|
||||
t.Errorf("no verifications without a configured key, got %+v", task.AP2Verifications)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAP2TamperCasesFailDistinctly(t *testing.T) {
|
||||
pub, priv := testAP2Key(t)
|
||||
rail := X402AP2Rail("payreq_123")
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -57,11 +56,9 @@ func TestClientSendAndCard(t *testing.T) {
|
||||
|
||||
func TestClientContinuesTaskAndConfiguresPush(t *testing.T) {
|
||||
card := Card("solo", "http://localhost:4000", "", []string{"task"})
|
||||
// The push receiver below is a loopback test server; authorize it as a
|
||||
// deployment would authorize its trusted push receiver.
|
||||
h := NewAgentHandler(card, func(_ context.Context, text string) (string, error) {
|
||||
return "echo:" + text, nil
|
||||
}, WithPushURLPolicy(func(*url.URL) error { return nil }))
|
||||
})
|
||||
ts := httptest.NewServer(h)
|
||||
defer ts.Close()
|
||||
|
||||
|
||||
@@ -1,131 +0,0 @@
|
||||
package a2a
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Push-notification callbacks are the one place the A2A gateway makes an
|
||||
// outbound HTTP request to an address chosen by a (possibly untrusted) caller:
|
||||
// tasks/pushNotificationConfig/set records a URL and deliverPush POSTs task
|
||||
// state to it. Without a guard that is a server-side request forgery vector —
|
||||
// a caller can aim the gateway at loopback, link-local (cloud metadata), or
|
||||
// private hosts it would otherwise never reach.
|
||||
//
|
||||
// The default policy allows only http/https callbacks whose host does not
|
||||
// resolve to a loopback, private, link-local, or unspecified address, and the
|
||||
// guarded HTTP client re-checks the *resolved* IP at dial time so a hostname
|
||||
// that passes validation cannot be rebound to an internal address before the
|
||||
// connection is made. Operators who need to reach a trusted in-cluster
|
||||
// receiver set Options.AllowPushURL to take over the policy.
|
||||
|
||||
// pushLookupIP resolves a host to IPs; overridable in tests.
|
||||
var pushLookupIP = net.LookupIP
|
||||
|
||||
// defaultPushURLPolicy is the SSRF-safe policy applied when no AllowPushURL is
|
||||
// configured. It rejects non-http(s) schemes and hosts that resolve to a
|
||||
// loopback, private, link-local, multicast, or unspecified address.
|
||||
func defaultPushURLPolicy(u *url.URL) error {
|
||||
switch u.Scheme {
|
||||
case "http", "https":
|
||||
default:
|
||||
return fmt.Errorf("push callback scheme %q not allowed (want http or https)", u.Scheme)
|
||||
}
|
||||
host := u.Hostname()
|
||||
if host == "" {
|
||||
return fmt.Errorf("push callback url has no host")
|
||||
}
|
||||
ips, err := resolvePushHost(host)
|
||||
if err != nil {
|
||||
return fmt.Errorf("push callback host %q: %w", host, err)
|
||||
}
|
||||
if len(ips) == 0 {
|
||||
return fmt.Errorf("push callback host %q did not resolve", host)
|
||||
}
|
||||
for _, ip := range ips {
|
||||
if blockedPushIP(ip) {
|
||||
return fmt.Errorf("push callback host %q resolves to a blocked address %s", host, ip)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func resolvePushHost(host string) ([]net.IP, error) {
|
||||
if ip := net.ParseIP(host); ip != nil {
|
||||
return []net.IP{ip}, nil
|
||||
}
|
||||
return pushLookupIP(host)
|
||||
}
|
||||
|
||||
// blockedPushIP reports whether ip is one an outbound push callback must not
|
||||
// reach: loopback, private (RFC1918 / ULA), link-local (incl. 169.254.169.254
|
||||
// cloud metadata), multicast, or the unspecified address.
|
||||
func blockedPushIP(ip net.IP) bool {
|
||||
return ip == nil ||
|
||||
ip.IsLoopback() ||
|
||||
ip.IsPrivate() ||
|
||||
ip.IsLinkLocalUnicast() ||
|
||||
ip.IsLinkLocalMulticast() ||
|
||||
ip.IsInterfaceLocalMulticast() ||
|
||||
ip.IsMulticast() ||
|
||||
ip.IsUnspecified()
|
||||
}
|
||||
|
||||
// pushDialControl runs after DNS resolution, immediately before connect, on the
|
||||
// resolved address — so it blocks a host that passed URL validation but was
|
||||
// rebound to an internal IP (DNS rebinding).
|
||||
func pushDialControl(_, address string, _ syscall.RawConn) error {
|
||||
host, _, err := net.SplitHostPort(address)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ip := net.ParseIP(host)
|
||||
if ip == nil {
|
||||
return fmt.Errorf("push callback: cannot parse dial address %q", address)
|
||||
}
|
||||
if blockedPushIP(ip) {
|
||||
return fmt.Errorf("push callback: refusing to connect to blocked address %s", ip)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// pushGuardClient is the HTTP client used for default-policy push delivery. Its
|
||||
// dialer refuses connections to blocked addresses at connect time.
|
||||
var pushGuardClient = &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
Transport: &http.Transport{
|
||||
Proxy: http.ProxyFromEnvironment,
|
||||
DialContext: (&net.Dialer{
|
||||
Timeout: 5 * time.Second,
|
||||
Control: pushDialControl,
|
||||
}).DialContext,
|
||||
},
|
||||
}
|
||||
|
||||
// checkPushURL validates a callback URL against the dispatcher's effective
|
||||
// policy (Options.AllowPushURL, or the default SSRF-safe policy).
|
||||
func (d *dispatcher) checkPushURL(raw string) error {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalid push callback url: %w", err)
|
||||
}
|
||||
policy := d.allowPushURL
|
||||
if policy == nil {
|
||||
policy = defaultPushURLPolicy
|
||||
}
|
||||
return policy(u)
|
||||
}
|
||||
|
||||
// pushClient is the HTTP client deliverPush uses: the guarded client under the
|
||||
// default policy, or the default client when an operator has taken over the
|
||||
// policy via Options.AllowPushURL (they own the trust decision then).
|
||||
func (d *dispatcher) pushClient() *http.Client {
|
||||
if d.guardPushDial {
|
||||
return pushGuardClient
|
||||
}
|
||||
return http.DefaultClient
|
||||
}
|
||||
@@ -1,147 +0,0 @@
|
||||
package a2a
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDefaultPushURLPolicy(t *testing.T) {
|
||||
// Resolve test hostnames deterministically without real DNS.
|
||||
orig := pushLookupIP
|
||||
pushLookupIP = func(host string) ([]net.IP, error) {
|
||||
switch host {
|
||||
case "internal.example":
|
||||
return []net.IP{net.ParseIP("10.1.2.3")}, nil
|
||||
case "public.example":
|
||||
return []net.IP{net.ParseIP("93.184.216.34")}, nil
|
||||
case "rebind.example":
|
||||
// A host that resolves to both a public and an internal IP must be
|
||||
// rejected — any blocked address is disqualifying.
|
||||
return []net.IP{net.ParseIP("93.184.216.34"), net.ParseIP("127.0.0.1")}, nil
|
||||
}
|
||||
return nil, &net.DNSError{Err: "no such host", Name: host, IsNotFound: true}
|
||||
}
|
||||
defer func() { pushLookupIP = orig }()
|
||||
|
||||
blocked := []string{
|
||||
"http://127.0.0.1/hook", // loopback
|
||||
"http://169.254.169.254/latest/meta", // cloud metadata (link-local)
|
||||
"http://10.0.0.5/hook", // RFC1918
|
||||
"http://[::1]/hook", // IPv6 loopback
|
||||
"http://[fd00::1]/hook", // IPv6 ULA (private)
|
||||
"http://0.0.0.0/hook", // unspecified
|
||||
"http://internal.example/hook", // hostname → private
|
||||
"http://rebind.example/hook", // one internal IP among many
|
||||
"ftp://public.example/hook", // non-http(s) scheme
|
||||
"file:///etc/passwd", // scheme
|
||||
"http:///nohost", // no host
|
||||
}
|
||||
for _, raw := range blocked {
|
||||
u, err := url.Parse(raw)
|
||||
if err != nil {
|
||||
t.Fatalf("parse %q: %v", raw, err)
|
||||
}
|
||||
if err := defaultPushURLPolicy(u); err == nil {
|
||||
t.Errorf("defaultPushURLPolicy(%q) = nil, want blocked", raw)
|
||||
}
|
||||
}
|
||||
|
||||
allowed := []string{
|
||||
"http://93.184.216.34/hook", // public literal IP
|
||||
"https://public.example/hook", // hostname → public
|
||||
}
|
||||
for _, raw := range allowed {
|
||||
u, _ := url.Parse(raw)
|
||||
if err := defaultPushURLPolicy(u); err != nil {
|
||||
t.Errorf("defaultPushURLPolicy(%q) = %v, want allowed", raw, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPushDialControlBlocksPrivate(t *testing.T) {
|
||||
blocked := []string{"127.0.0.1:80", "169.254.169.254:80", "10.0.0.1:443", "[::1]:80", "0.0.0.0:80"}
|
||||
for _, addr := range blocked {
|
||||
if err := pushDialControl("tcp", addr, nil); err == nil {
|
||||
t.Errorf("pushDialControl(%q) = nil, want blocked", addr)
|
||||
}
|
||||
}
|
||||
if err := pushDialControl("tcp", "8.8.8.8:443", nil); err != nil {
|
||||
t.Errorf("pushDialControl(public) = %v, want allowed", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestSetPushConfigRejectsSSRFURL: an untrusted caller cannot register a
|
||||
// callback pointing at an internal address — it is refused and nothing stored.
|
||||
func TestSetPushConfigRejectsSSRFURL(t *testing.T) {
|
||||
d := newDispatcher()
|
||||
d.store(&Task{ID: "t1", ContextID: "c1", Status: TaskStatus{State: stateCompleted}})
|
||||
|
||||
params, _ := json.Marshal(map[string]any{
|
||||
"id": "t1",
|
||||
"pushNotificationConfig": map[string]any{"url": "http://169.254.169.254/latest/meta-data"},
|
||||
})
|
||||
rr := httptest.NewRecorder()
|
||||
d.setPushConfig(rr, rpcRequest{JSONRPC: "2.0", ID: json.RawMessage("1"), Params: params})
|
||||
|
||||
var resp rpcResponse
|
||||
if err := json.Unmarshal(rr.Body.Bytes(), &resp); err != nil {
|
||||
t.Fatalf("decode: %v", err)
|
||||
}
|
||||
if resp.Error == nil || resp.Error.Code != errInvalidParams {
|
||||
t.Fatalf("response = %+v, want invalid-params rejection", resp)
|
||||
}
|
||||
d.mu.Lock()
|
||||
_, stored := d.pushConfigs["t1"]
|
||||
d.mu.Unlock()
|
||||
if stored {
|
||||
t.Error("SSRF callback url must not be stored")
|
||||
}
|
||||
}
|
||||
|
||||
// TestDeliverPushBlocksInternalByDefault: even if a config for an internal URL
|
||||
// slips into the map, deliverPush must not POST to it under the default policy.
|
||||
func TestDeliverPushBlocksInternalByDefault(t *testing.T) {
|
||||
var hit bool
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { hit = true }))
|
||||
defer srv.Close() // srv.URL is http://127.0.0.1:PORT — loopback, must be blocked
|
||||
|
||||
d := newDispatcher()
|
||||
task := &Task{ID: "t1", Status: TaskStatus{State: stateCompleted}}
|
||||
d.pushConfigs["t1"] = PushNotificationConfig{URL: srv.URL}
|
||||
|
||||
d.deliverPush("t1", task)
|
||||
if hit {
|
||||
t.Error("deliverPush reached a loopback callback under the default policy")
|
||||
}
|
||||
}
|
||||
|
||||
// TestAllowPushURLOverrideDelivers: an operator policy can authorize a trusted
|
||||
// (here loopback) receiver, and delivery then goes through.
|
||||
func TestAllowPushURLOverrideDelivers(t *testing.T) {
|
||||
done := make(chan struct{}, 1)
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Header.Get("Content-Type") == "application/json" {
|
||||
done <- struct{}{}
|
||||
}
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
g := New(Options{AllowPushURL: func(*url.URL) error { return nil }})
|
||||
d := g.disp
|
||||
if d.guardPushDial {
|
||||
t.Fatal("custom AllowPushURL should disable the dial guard")
|
||||
}
|
||||
task := &Task{ID: "t1", Status: TaskStatus{State: stateCompleted}}
|
||||
d.pushConfigs["t1"] = PushNotificationConfig{URL: srv.URL}
|
||||
|
||||
d.deliverPush("t1", task)
|
||||
select {
|
||||
case <-done:
|
||||
default:
|
||||
t.Error("operator-authorized callback was not delivered")
|
||||
}
|
||||
}
|
||||
@@ -150,71 +150,29 @@ See [Native gRPC Compatibility](grpc-compatibility.md) for a complete guide.
|
||||
|
||||
## vs Dapr
|
||||
|
||||
[Dapr](https://dapr.io/) is a distributed application runtime. Its building
|
||||
blocks cover service invocation, state, pub/sub, bindings, secrets,
|
||||
configuration, distributed locks, actors, jobs, and workflow, usually accessed
|
||||
through a sidecar from many languages. [Dapr Agents](https://docs.dapr.io/developing-ai/dapr-agents/)
|
||||
adds an agent framework on top of those runtime capabilities.
|
||||
### Dapr Approach
|
||||
- Multi-language via sidecar
|
||||
- Rich building blocks (state, pub/sub, bindings)
|
||||
- Cloud-native focused
|
||||
- Requires running sidecar process
|
||||
|
||||
Go Micro overlaps with Dapr on distributed-systems primitives, but the product
|
||||
shape is different: Go Micro is a Go framework where services, agents, tools,
|
||||
and flows are built from the same runtime. A service endpoint can become an
|
||||
AI-callable tool, and an agent is itself a registered service with memory,
|
||||
guardrails, planning, delegation, MCP, and A2A around it.
|
||||
### Go Micro Approach
|
||||
- Go library, no sidecar
|
||||
- Direct service-to-service calls
|
||||
- Simpler deployment
|
||||
- Lower latency (no extra hop)
|
||||
|
||||
### Decision table
|
||||
### When to Choose Dapr
|
||||
- You have polyglot services (Node, Python, Java, etc)
|
||||
- You want portable abstractions across clouds
|
||||
- You're fully on Kubernetes
|
||||
- You need state management abstractions
|
||||
|
||||
| Need | Prefer Go Micro | Prefer Dapr | Use both |
|
||||
|---|---|---|---|
|
||||
| **Primary language** | Your core runtime is Go and you want library-native APIs | You run a polyglot estate and want one sidecar API across languages | Go services use Go Micro while non-Go services expose Dapr APIs |
|
||||
| **Agent model** | Agents should be ordinary services: registered, discoverable, callable by RPC, MCP, and A2A | Agents are primarily Python applications using Dapr Agents | Dapr-hosted agents call Go Micro MCP tools, or Go Micro agents call Dapr-backed services |
|
||||
| **Tools** | Existing service endpoints should become tools with minimal extra code | Tools are modeled through Dapr components, bindings, or agent framework code | Use Dapr components behind Go Micro services that expose a stable tool surface |
|
||||
| **Workflows** | Deterministic steps should live beside Go services and agents in the same codebase | You want Dapr Workflow's sidecar-backed orchestration model across languages | Let Dapr own cross-language workflows and let Go Micro own Go-native agent/tool execution |
|
||||
| **State and pub/sub** | You want Go interfaces and pluggable packages directly in-process | You want component YAML and sidecar portability across backing services | Put portable infrastructure behind Dapr and domain/tool logic in Go Micro |
|
||||
| **Deployment** | You want a simple Go binary/runtime first, with Kubernetes support as an explicit deployment target | You are already standardized on Dapr sidecars in Kubernetes | Run Go Micro services in clusters that already have Dapr for shared infrastructure |
|
||||
| **Interop** | MCP and A2A are first-class requirements for exposing services and agents | Dapr's app APIs and agent framework are the integration boundary | Bridge through MCP/A2A at the agent edge and Dapr APIs at the infrastructure edge |
|
||||
|
||||
### When to choose Dapr
|
||||
|
||||
- You need a **polyglot** runtime contract for Node, Python, Java, .NET, Go, and
|
||||
other services.
|
||||
- Your platform team already operates sidecars and component configuration across
|
||||
Kubernetes clusters.
|
||||
- You want Dapr's standard building blocks for state, pub/sub, bindings, secrets,
|
||||
actors, jobs, and workflow more than you want a Go-native service framework.
|
||||
- You are adopting Dapr Agents and want to stay in its Python-first agent stack.
|
||||
|
||||
### When to choose Go Micro
|
||||
|
||||
- You are building mostly in Go and want the agent harness to be the same runtime
|
||||
as your services.
|
||||
- You want service methods and their comments/examples to become AI-callable tools
|
||||
without maintaining a separate tool layer.
|
||||
- You want agents to be deployed, discovered, called, load-balanced, and inspected
|
||||
like ordinary services.
|
||||
- You need MCP and A2A at the agent/service boundary, not only an internal
|
||||
application API.
|
||||
- You prefer library-native composition and direct Go interfaces over sidecar
|
||||
component wiring.
|
||||
|
||||
### Where Go Micro still needs to prove itself
|
||||
|
||||
Dapr has a mature platform narrative and broad deployment footprint. Go Micro's
|
||||
agent-harness story is sharper for Go teams, but production adoption depends on
|
||||
keeping the no-secret getting-started path green, documenting durability
|
||||
semantics clearly, proving MCP/A2A conformance with external clients, and making
|
||||
Kubernetes deployment first-class.
|
||||
|
||||
### Practical migration path
|
||||
|
||||
1. Start with one Go Micro service that wraps a real domain capability.
|
||||
2. Add doc comments and examples so the endpoint is useful as an agent tool.
|
||||
3. Expose it through MCP for external agents or through A2A if the capability is
|
||||
itself an agent.
|
||||
4. If your platform already uses Dapr, keep Dapr components behind the service
|
||||
boundary and let Go Micro present the agent/tool contract.
|
||||
5. Move deterministic multi-step work into flows only after the service/tool
|
||||
boundary is stable.
|
||||
### When to Choose Go Micro
|
||||
- You're building Go services
|
||||
- You want lower latency
|
||||
- You prefer libraries over sidecars
|
||||
- You want simpler deployment (no sidecar management)
|
||||
|
||||
## vs Agent Frameworks (Google ADK)
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ Otherwise continue to read the docs for more information about the framework.
|
||||
|
||||
## Advanced
|
||||
|
||||
- [Framework Comparison](guides/comparison.html) - Including Go Micro vs Dapr for agents, services, and workflows
|
||||
- [Framework Comparison](guides/comparison.html)
|
||||
- [Architecture Decisions](architecture/)
|
||||
- [Real-World Examples](examples/realworld/)
|
||||
- [Migration Guides](guides/migration/)
|
||||
|
||||
Reference in New Issue
Block a user