项目文件夹

文件
Asim Aslam ce0741a80c Implement tool-execution wrappers and restructure ToolHandler (#2970)
* 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>
2026-06-16 17:26:30 +01:00

2.1 KiB

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:

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.

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 for the full picture.

Run

Needs an LLM provider key:

export ANTHROPIC_API_KEY=sk-ant-...   # or OPENAI_API_KEY, GEMINI_API_KEY, ...
go run main.go