e071084ebe
govulncheck / govulncheck (push) Has been cancelled
Lint / golangci-lint (push) Has been cancelled
Run Tests / Unit Tests (push) Has been cancelled
Run Tests / Etcd Integration Tests (push) Has been cancelled
Harness (E2E) / Harnesses (mock LLM) (push) Has been cancelled
Harness (E2E) / Provider harnesses (live LLM conformance) (push) Has been cancelled
297 lines
10 KiB
Markdown
297 lines
10 KiB
Markdown
# Agent Interface Design
|
|
|
|
## Principle
|
|
|
|
Service = capability. Agent = intelligence. An agent IS a service — it has a real RPC server, a proto-defined `Agent.Chat` endpoint, and registers in the registry like everything else.
|
|
|
|
```
|
|
micro.NewService("task") // creates a service
|
|
micro.NewAgent("task-mgr") // creates an agent (which is also a service)
|
|
```
|
|
|
|
Same package. Same level. Same communication (RPC). Different responsibilities.
|
|
|
|
## Interface
|
|
|
|
```go
|
|
type Agent interface {
|
|
Name() string
|
|
Init(...AgentOption)
|
|
Options() AgentOptions
|
|
Ask(ctx context.Context, message string) (*Response, error)
|
|
Run() error
|
|
Stop() error
|
|
String() string
|
|
}
|
|
```
|
|
|
|
**Ask** is the programmatic API. Send a message, get a response.
|
|
|
|
**Run** starts a real RPC server, registers the `Agent.Chat` endpoint in the registry, and blocks.
|
|
|
|
## Proto Definition
|
|
|
|
```protobuf
|
|
service Agent {
|
|
rpc Chat(ChatRequest) returns (ChatResponse) {}
|
|
}
|
|
|
|
message ChatRequest {
|
|
string message = 1;
|
|
}
|
|
|
|
message ChatResponse {
|
|
string reply = 1;
|
|
string agent = 2;
|
|
repeated ToolCall tool_calls = 3;
|
|
}
|
|
```
|
|
|
|
The agent is callable by any go-micro client:
|
|
|
|
```bash
|
|
micro call task-mgr Agent.Chat '{"message": "What tasks are overdue?"}'
|
|
```
|
|
|
|
## Options
|
|
|
|
```go
|
|
type AgentOptions struct {
|
|
Name string
|
|
Services []string // which services this agent manages
|
|
Prompt string // system prompt — identity, domain knowledge, boundaries
|
|
Provider string // LLM provider (anthropic, openai, etc.)
|
|
Model string // LLM model (optional)
|
|
APIKey string
|
|
Registry registry.Registry // discover services and other agents
|
|
Client client.Client // call service endpoints and other agents
|
|
Store store.Store // backing store for the default memory
|
|
HistoryLimit int // max conversation turns to retain
|
|
Memory Memory // pluggable conversation memory (default: store-backed)
|
|
MaxSteps int // stopping condition: max tool calls per Ask
|
|
LoopLimit int // max identical repeated calls before refusal (default 3)
|
|
Approve ApproveFunc // human-in-the-loop / policy gate on each action
|
|
}
|
|
```
|
|
|
|
## Pluggable composition
|
|
|
|
An agent composes the same way a service does — a small set of pluggable pieces with working defaults:
|
|
|
|
| Piece | Default | Swap with |
|
|
|-------|---------|-----------|
|
|
| **Model** | first registered provider | `AgentProvider` / `AgentModel` |
|
|
| **Memory** | store-backed, durable across restarts | `AgentMemory(m Memory)` |
|
|
| **Tools** | the agent's services (RPC) + `plan`/`delegate` | `AgentTool(name, desc, schema, fn)` for any function |
|
|
| **Guardrails** | loop detection on | `AgentMaxSteps`, `AgentLoopLimit`, `AgentApproveTool` |
|
|
| **Tool middleware** | none | `AgentWrapTool(...ai.ToolWrapper)` — wrap tool execution (logging, metrics, retries) |
|
|
|
|
```go
|
|
type Memory interface {
|
|
Add(role, content string)
|
|
Messages() []ai.Message
|
|
Clear()
|
|
}
|
|
```
|
|
|
|
`NewMemory(store, key, limit)` is the durable default; `NewInMemory(limit)` is non-persistent. `AgentTool` registers a function the model can call alongside the services it discovers.
|
|
|
|
Functional options:
|
|
|
|
```go
|
|
agent := micro.NewAgent("task-mgr",
|
|
micro.AgentServices("task"),
|
|
micro.AgentPrompt("You manage tasks. You understand deadlines and priorities."),
|
|
micro.AgentProvider("anthropic"),
|
|
)
|
|
```
|
|
|
|
## Scoped Tools
|
|
|
|
An agent only sees the endpoints of its assigned services (plus excludes its own endpoints so it doesn't call itself).
|
|
|
|
## Memory
|
|
|
|
Agents persist conversation history in the store. Memory survives restarts.
|
|
Each agent's state is confined to its own store table (database `agent`,
|
|
table `{name}`) via `store.Scope`, so agents don't share a global table
|
|
with each other, with services, or with flows.
|
|
|
|
```
|
|
database "agent", table "{name}":
|
|
history — conversation history
|
|
```
|
|
|
|
## Durable Ask / StreamAsk runs
|
|
|
|
Agents can opt into the same checkpoint backend used by flows with
|
|
`micro.AgentWithCheckpoint(...)`. When enabled, each `Ask` or `StreamAsk` run is
|
|
persisted with its input, terminal status, response, and tool-call records. If
|
|
the process or transport drops after a tool has completed but before the model
|
|
returns a final answer, restart the agent with the same checkpoint store and
|
|
call `micro.AgentResume(ctx, ag, runID)` or
|
|
`micro.AgentResumeStreamAsk(ctx, ag, runID)`.
|
|
Completed tool calls are served from the checkpoint instead of being executed
|
|
again, while guardrails such as `MaxSteps`, loop detection, approval pauses, and
|
|
`request_input` pauses continue to apply to the resumed run.
|
|
|
|
## Built-in Capabilities
|
|
|
|
Beyond its scoped service tools, every agent gets two built-in tools. They are not service endpoints — they are capabilities the agent has over itself and over other agents. They are plain tools wired into the agent's tool handler; there is no separate harness, loop engine, or graph. The LLM calls them exactly like any other tool.
|
|
|
|
### plan
|
|
|
|
For multi-step work the agent records an ordered plan: a list of steps, each with a `task` and a `status` (`pending`, `in_progress`, `done`). The plan is persisted to the store and surfaced back in the system prompt on later turns, so the agent stays oriented.
|
|
|
|
```
|
|
database "agent", table "{name}":
|
|
plan — current plan
|
|
```
|
|
|
|
### delegate
|
|
|
|
The agent hands a self-contained subtask to another agent. **Delegate-first** resolution:
|
|
|
|
1. If the target names a **registered agent** (a service advertising `type=agent`), the subtask is sent to it via RPC (`Agent.Chat`). Intelligence stays distributed — the domain expert handles its own services.
|
|
2. Otherwise a focused **ephemeral sub-agent** is created with `New(...)` + `Ask(...)`, given a fresh, isolated context, asked the subtask, and torn down.
|
|
|
|
A sub-agent is just an agent — no new "spawn"/"fork" concept. Ephemeral sub-agents load and persist no history and have no built-in tools, so they cannot plan or re-delegate (which bounds recursion).
|
|
|
|
These capabilities are added automatically to any non-ephemeral agent, so existing `NewAgent` services and `micro chat` routing get them for free.
|
|
|
|
## Registration
|
|
|
|
Agents register as real services via `server.NewServer` with metadata:
|
|
|
|
```go
|
|
server.Metadata(map[string]string{
|
|
"type": "agent",
|
|
"services": "task,project",
|
|
})
|
|
```
|
|
|
|
The server has a real address, real transport, real endpoints. `micro agent list` discovers agents by checking server metadata for `type=agent`.
|
|
|
|
## The Router (micro chat)
|
|
|
|
`micro chat` is a router. It discovers agents from the registry and dispatches to them via RPC.
|
|
|
|
- One agent → routes directly via `client.Call(agentName, "Agent.Chat", ...)`
|
|
- Multiple agents → LLM classifies intent, calls `route_to_agent` tool
|
|
- No agents → falls back to direct service access (current behaviour)
|
|
|
|
## Agent-to-Agent Communication
|
|
|
|
Agents call each other via standard RPC. An agent is a service — it has an `Agent.Chat` endpoint. Any agent can call any other agent the same way it calls a service.
|
|
|
|
```go
|
|
// From inside an agent's logic, call another agent:
|
|
client.Call("comms-mgr", "Agent.Chat", &ChatRequest{Message: "Notify Alice"})
|
|
```
|
|
|
|
No special protocol. No broker topics. Just RPC.
|
|
|
|
### Across frameworks: the A2A gateway
|
|
|
|
That covers agents *within* a Go Micro system. To reach agents on other
|
|
frameworks — and to let them reach yours — there is the **A2A gateway**
|
|
(`gateway/a2a`, run with `micro a2a serve`), the agent-side analogue of
|
|
the MCP gateway. It discovers agents from the registry, generates an
|
|
[Agent Card](https://a2a-protocol.org) for each from its metadata (the
|
|
same way MCP derives tools), and translates incoming Agent2Agent tasks to
|
|
the agent's `Agent.Chat` RPC. No per-agent code: register an agent and it
|
|
is reachable over A2A.
|
|
|
|
## Usage Patterns
|
|
|
|
### Single-service agent
|
|
|
|
```go
|
|
agent := micro.NewAgent("task-mgr",
|
|
micro.AgentServices("task"),
|
|
micro.AgentPrompt("You manage tasks."),
|
|
micro.AgentProvider("anthropic"),
|
|
)
|
|
agent.Run()
|
|
```
|
|
|
|
### Multi-service agent
|
|
|
|
```go
|
|
agent := micro.NewAgent("project-mgr",
|
|
micro.AgentServices("task", "project", "milestone"),
|
|
micro.AgentPrompt("You manage the project system."),
|
|
micro.AgentProvider("anthropic"),
|
|
)
|
|
agent.Run()
|
|
```
|
|
|
|
### Programmatic
|
|
|
|
```go
|
|
agent := micro.NewAgent("support", ...)
|
|
agent.Init()
|
|
resp, _ := agent.Ask(ctx, "What tickets are open?")
|
|
```
|
|
|
|
### Agent alongside service
|
|
|
|
```go
|
|
func main() {
|
|
svc := micro.NewService("task")
|
|
svc.Handle(new(TaskHandler))
|
|
|
|
agent := micro.NewAgent("task-mgr",
|
|
micro.AgentServices("task"),
|
|
micro.AgentPrompt("You manage tasks."),
|
|
micro.AgentProvider("anthropic"),
|
|
)
|
|
|
|
go svc.Run()
|
|
agent.Run()
|
|
}
|
|
```
|
|
|
|
## CLI
|
|
|
|
```bash
|
|
micro agent list # list running agents (registry, type=agent)
|
|
micro agent describe task-mgr # show agent details
|
|
micro agent history task-mgr # show stored conversation (scoped store)
|
|
micro chat # routes to agents automatically
|
|
micro call task-mgr Agent.Chat '{"message": "..."}' # direct RPC
|
|
```
|
|
|
|
The split is consistent across services, agents, and flows: **`list`**
|
|
shows what's *running* (from the registry, filtered by type), while
|
|
**`history`/`runs`** show *durable* state from the scoped store —
|
|
available whether or not the component is currently running.
|
|
|
|
```bash
|
|
micro flow list # list running flows (registry, type=flow)
|
|
micro flow runs checkout # durable run history for a flow (scoped store)
|
|
```
|
|
|
|
## Generation
|
|
|
|
`micro run --prompt` creates services AND an agent:
|
|
|
|
```
|
|
micro run --prompt "task management system"
|
|
|
|
Generated:
|
|
task/ ← service
|
|
project/ ← service
|
|
agent/ ← agent (manages task, project)
|
|
```
|
|
|
|
The agent reads `MICRO_AI_PROVIDER` and `MICRO_AI_API_KEY` from the environment.
|
|
|
|
## What Doesn't Change
|
|
|
|
- Services are still services — same interface, same code, same deployment
|
|
- You can run services without agents
|
|
- You can call services directly via `micro call`, the API, or MCP
|
|
- The framework interfaces (registry, client, server, store) are unchanged
|
|
- `micro run`, `micro deploy`, `micro build` work the same way
|