mirror of
https://github.com/go-micro/go-micro.git
synced 2026-06-15 19:35:13 +02:00
* docs: map go-micro onto Anthropic's workflows-vs-agents taxonomy - new guide 'Agents and Workflows': adopts Anthropic's Building Effective Agents vocabulary — workflow (predefined path) = flow, agent (dynamic self-direction) = agent — maps the augmented-LLM building block and the five workflow patterns onto go-micro, and shows routing (chat router) and orchestrator-workers (conductor + plan/delegate) are already native. - flow package doc reframed as a workflow (predefined path) per the same taxonomy, with guidance on flow vs agent. - nav + README link the new guide. * feat: agent guardrails — step limit and tool approval hook Anthropic's Building Effective Agents stresses stopping conditions and human-in-the-loop checkpoints for autonomous agents. Add both as plain options enforced at the tool-handler choke point — no provider changes, no new abstraction: - MaxSteps(n): bound tool executions per Ask; beyond the limit, actions are refused and the model is told to stop and summarize. - ApproveTool(fn): gate each action before it runs; returning false blocks it and surfaces the reason to the model. The internal plan tool is never gated. Exposed at the micro package (AgentMaxSteps, AgentApproveTool, ApproveFunc). Tests cover the limit, blocking, and that plan is not gated. Guardrails section of the agents-and-workflows guide updated from 'active work' to documented options. * feat: flow can dispatch to an agent (flow triggers, agent reasons) Unify the engine without collapsing the workflow/agent distinction. A Flow with Agent set hands each event's rendered prompt to a named registered agent over RPC (Agent.Chat) instead of running its own LLM step — so the workflow stays the deterministic trigger and the agent is the reasoning engine, with its plan, delegate, memory, and guardrails. A plain flow is unchanged (single augmented-LLM step). - flow.Agent(name) / micro.FlowAgent(name); flow stores the client and skips model setup when dispatching. - test: dispatch routes to comms.Agent.Chat with the rendered prompt and records the reply. - guide: 'Flow triggers, Agent reasons' section. --------- Co-authored-by: Claude <noreply@anthropic.com>
85 lines
2.5 KiB
Go
85 lines
2.5 KiB
Go
package agent
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"go-micro.dev/v5/registry"
|
|
"go-micro.dev/v5/store"
|
|
)
|
|
|
|
// MaxSteps refuses tool calls once the per-Ask limit is exceeded; plan
|
|
// is bookkeeping and is never counted.
|
|
func TestMaxStepsStopsActions(t *testing.T) {
|
|
a := newTestAgent(Name("limited"), MaxSteps(2))
|
|
|
|
h := a.toolHandler()
|
|
|
|
// plan must not consume a step.
|
|
a.steps = 0
|
|
h(toolPlan, map[string]any{"steps": []any{}})
|
|
if a.steps != 0 {
|
|
t.Fatalf("plan consumed a step: steps=%d", a.steps)
|
|
}
|
|
|
|
// First two actions are allowed (they fall through to RPC, which
|
|
// fails harmlessly — we only care they weren't refused by the limit).
|
|
for i := 1; i <= 2; i++ {
|
|
_, content := h("demo_Svc_Do", map[string]any{})
|
|
if strings.Contains(content, "step limit") {
|
|
t.Fatalf("action %d wrongly hit the step limit", i)
|
|
}
|
|
}
|
|
|
|
// Third action exceeds MaxSteps(2) and must be refused.
|
|
_, content := h("demo_Svc_Do", map[string]any{})
|
|
if !strings.Contains(content, "step limit") {
|
|
t.Errorf("third action should hit the step limit; got %q", content)
|
|
}
|
|
}
|
|
|
|
// ApproveTool blocks an action when the hook denies it, and the denial
|
|
// reason is surfaced to the model.
|
|
func TestApproveToolBlocks(t *testing.T) {
|
|
var sawTool string
|
|
a := newTestAgent(Name("gated"),
|
|
ApproveTool(func(tool string, input map[string]any) (bool, string) {
|
|
sawTool = tool
|
|
return false, "needs sign-off"
|
|
}),
|
|
)
|
|
|
|
_, content := a.toolHandler()("demo_Svc_Do", map[string]any{})
|
|
if sawTool != "demo_Svc_Do" {
|
|
t.Errorf("approver saw %q, want demo_Svc_Do", sawTool)
|
|
}
|
|
if !strings.Contains(content, "not approved") || !strings.Contains(content, "needs sign-off") {
|
|
t.Errorf("blocked call should surface the reason; got %q", content)
|
|
}
|
|
}
|
|
|
|
// A denying approver must not gate the internal plan tool.
|
|
func TestApproveToolDoesNotGatePlan(t *testing.T) {
|
|
mem := store.NewMemoryStore()
|
|
a := New(
|
|
Name("gated"),
|
|
Provider("fake"),
|
|
WithRegistry(registry.NewMemoryRegistry()),
|
|
WithStore(mem),
|
|
ApproveTool(func(tool string, input map[string]any) (bool, string) {
|
|
return false, "deny everything"
|
|
}),
|
|
).(*agentImpl)
|
|
a.setup()
|
|
|
|
_, content := a.toolHandler()(toolPlan, map[string]any{
|
|
"steps": []any{map[string]any{"task": "x", "status": "pending"}},
|
|
})
|
|
if strings.Contains(content, "not approved") {
|
|
t.Errorf("plan must not be gated by ApproveTool; got %q", content)
|
|
}
|
|
if recs, _ := mem.Read("agent/gated/plan"); len(recs) == 0 {
|
|
t.Error("plan should have been persisted despite the denying approver")
|
|
}
|
|
}
|