micro--go-micro
ce0741a80c
* feat(agent): tool-execution wrappers via WrapTool
Restructure ai.ToolHandler to the structured, ctx-carrying shape that
mirrors a go-micro RPC handler:
func(ctx context.Context, call ai.ToolCall) ai.ToolResult
This reuses the existing ToolCall (with its correlation ID) and
ToolResult types instead of the flat (name, input)->(any, string)
signature, and adds ToolCall.Scan for typed argument access.
Add ai.ToolWrapper and the agent option WrapTool / micro.AgentWrapTool —
the tool-side analogue of client.CallWrapper and server.HandlerWrapper.
Reframe the built-in guardrails (MaxSteps, LoopLimit, ApproveTool) as
composed wrappers around a base handler; developer wrappers compose
outermost, so they observe every call and result, including refusals.
Update all provider call sites, the MCP server and chat handlers, the
integration harnesses, and docs to the new signature.
* examples: add agent-wrap-tool showing AgentWrapTool
A runnable example of tool-execution middleware: an observe wrapper that
times calls and records per-tool metrics (correlated by call ID), and a
retry wrapper that recovers a flaky service call before the model sees
it. Demonstrates outermost-first composition and the wrapper/guardrail
interaction (retries are seen by loop detection).
---------
Co-authored-by: Claude <noreply@anthropic.com>
64 行
2.1 KiB
Markdown
64 行
2.1 KiB
Markdown
# Agent Tool Wrappers
|
|
|
|
Middleware around an agent's tool execution, the same way
|
|
`client.CallWrapper` and `server.HandlerWrapper` wrap RPCs.
|
|
|
|
Every tool call an agent makes runs through `ai.ToolHandler`:
|
|
|
|
```go
|
|
type ToolHandler func(ctx context.Context, call ai.ToolCall) ai.ToolResult
|
|
type ToolWrapper func(ai.ToolHandler) ai.ToolHandler
|
|
```
|
|
|
|
`WrapTool` (exposed as `micro.AgentWrapTool`) registers a wrapper: it
|
|
takes the next handler and returns a new one. Code before `next(...)`
|
|
runs before the tool, code after runs after. That single seam covers
|
|
the whole lifecycle — before/after hooks, timing, metrics, retries,
|
|
inspecting results.
|
|
|
|
## What this example does
|
|
|
|
One flaky `weather` service and one agent with two wrappers:
|
|
|
|
- **observe** — times every call and records a per-tool count, logging
|
|
the correlation ID (`call.ID`) carried through from the provider. It
|
|
observes; it changes nothing.
|
|
- **retry** — re-runs a call whose result is an error, up to three
|
|
attempts. The weather service fails the first time it's hit and
|
|
succeeds after, so retry turns a transient failure into a success the
|
|
model never sees.
|
|
|
|
Wrappers compose **outermost-first**: `observe` is registered first, so
|
|
it wraps `retry` and sees one logical call even when retry runs the tool
|
|
twice.
|
|
|
|
```go
|
|
micro.NewAgent("forecaster",
|
|
micro.AgentServices("weather"),
|
|
micro.AgentProvider(provider),
|
|
micro.AgentAPIKey(apiKey),
|
|
micro.AgentWrapTool(m.observe, retry(3)),
|
|
)
|
|
```
|
|
|
|
## Wrappers vs. guardrails
|
|
|
|
Developer wrappers run **outside** the built-in guardrails (`MaxSteps`,
|
|
`LoopLimit`, `ApproveTool`), so they see every call and its result —
|
|
including a guardrail's refusal. The flip side: a retry wrapper's
|
|
`next` is the full guardrail stack, so each retry is also counted by
|
|
loop detection. Keep `LoopLimit` at or above your retry count, or set
|
|
`AgentLoopLimit(0)` when a wrapper owns the repetition.
|
|
|
|
See the [Agent Guardrails guide](../../internal/website/docs/guides/agent-guardrails.md)
|
|
for the full picture.
|
|
|
|
## Run
|
|
|
|
Needs an LLM provider key:
|
|
|
|
```bash
|
|
export ANTHROPIC_API_KEY=sk-ant-... # or OPENAI_API_KEY, GEMINI_API_KEY, ...
|
|
go run main.go
|
|
```
|