package agent import ( "context" "crypto/sha256" "encoding/json" "fmt" "io" "net/http" "strings" "time" "go-micro.dev/v6/ai" codecBytes "go-micro.dev/v6/codec/bytes" "go-micro.dev/v6/gateway/a2a" "go-micro.dev/v6/store" "go-micro.dev/v6/wrapper/x402" ) // Built-in agent tools. These are not service endpoints — they are // capabilities the agent has over itself: maintaining a plan in its // memory, and delegating a subtask to another agent. // // They are plain tools, wired into the agent's tool handler alongside // the discovered service tools. There is no separate harness or graph: // the LLM calls them like any other tool. const ( toolPlan = "plan" toolDelegate = "delegate" toolHumanInput = "request_input" ) type delegateCall struct { done chan struct{} res ai.ToolResult } // builtinTools returns the tool definitions exposed to the model in // addition to the agent's scoped service tools. func builtinTools() []ai.Tool { return []ai.Tool{ { Name: toolPlan, OriginalName: toolPlan, Description: "Record or update your plan as an ordered list of steps before doing multi-step work. " + "Call this whenever the plan changes. The plan is saved to your memory and shown back to you on later turns.", Properties: map[string]any{ "steps": map[string]any{ "type": "array", "description": "Ordered plan steps. Each step has a 'task' (string) and a " + "'status' (one of: pending, in_progress, done).", }, }, }, { Name: toolHumanInput, OriginalName: toolHumanInput, Description: "Pause this agent run when you need missing information, a decision, or other human input before you can continue. " + "The run is checkpointed as input-required and can be resumed with the human response without losing completed tool history.", Properties: map[string]any{ "prompt": map[string]any{ "type": "string", "description": "The specific question, decision, or instruction needed from the human operator.", }, }, }, { Name: toolDelegate, OriginalName: toolDelegate, Description: "Delegate a self-contained subtask to another agent. If 'to' names an agent that already " + "manages the relevant services, that agent handles it; otherwise a focused sub-agent is created for the " + "subtask. The sub-agent works in an isolated context and returns only its result. Use this to keep your " + "own context focused and to let domain experts handle their own services.", Properties: map[string]any{ "task": map[string]any{ "type": "string", "description": "The subtask to delegate, described completely and self-contained.", }, "to": map[string]any{ "type": "string", "description": "Optional. The agent or service name best suited to the subtask, or the URL of an external agent that speaks the A2A protocol.", }, }, }, } } // Builtins returns the built-in agent tools (plan, delegate) together // with a handler for them, so the same capabilities can be wired into a // tool loop that isn't a running Agent — for example the `micro chat` // fallback. The handler's third return value is false when the name is // not a built-in, so callers can fall through to their own tools. // // Configure it with the same options as an Agent (Name, Provider, // WithStore, WithRegistry, WithClient, ...); these back plan's memory // and delegate's RPC/sub-agent behavior. func Builtins(opts ...Option) (tools []ai.Tool, handle func(name string, input map[string]any) (result any, content string, ok bool)) { a := &agentImpl{opts: newOptions(opts...)} handle = func(name string, input map[string]any) (any, string, bool) { switch name { case toolPlan: r := a.handlePlan(ai.ToolCall{Name: name, Input: input}) return r.Value, r.Content, true case toolHumanInput: r := a.handleHumanInput(ai.ToolCall{Name: name, Input: input}) return r.Value, r.Content, true case toolDelegate: r := a.handleDelegate(context.Background(), ai.ToolCall{Name: name, Input: input}) return r.Value, r.Content, true } return nil, "", false } return builtinTools(), handle } // toolHandler returns the agent's tool-call handler, composed as a stack // of wrappers around a base handler — the same middleware shape as // client/server wrappers. The base executes the call (custom tools, // delegate, or RPC); the built-in guardrails wrap it; developer wrappers // (WrapTool) wrap those, outermost, so they observe every call and its // result including guardrail refusals. Ephemeral sub-agents get the bare // service handler so they can neither plan nor re-delegate (which // prevents runaway recursion). func (a *agentImpl) toolHandler() ai.ToolHandler { if a.ephemeral { return a.toolTimeoutWrap(a.tools.Handler()) } // Innermost first: base, then guardrails (approve → loop → step → // plan), then developer wrappers outermost. Wrapping reverses order, // so the result runs plan → step → loop → approve → checkpoint → base. h := a.baseHandler() h = a.toolTimeoutWrap(h) h = a.x402PayWrap(h) h = a.toolRetryWrap(h) h = a.checkpointToolWrap(h) h = a.approveWrap(h) h = a.spendWrap(h) h = a.loopWrap(h) h = a.stepWrap(h) h = a.planWrap(h) h = contextWrap(h) h = a.traceTool(h) for i := len(a.opts.wrappers) - 1; i >= 0; i-- { h = a.opts.wrappers[i](h) } return h } // contextWrap stops tool execution promptly when the Ask context has // already been canceled or its deadline has expired. This keeps guardrail // bookkeeping and side-effecting tools from running after the caller has // abandoned the agent run. func contextWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { select { case <-ctx.Done(): return errResult(call.ID, ctx.Err().Error()) default: } return next(ctx, call) } } // toolTimeoutWrap gives each tool execution its own deadline while preserving // caller cancellation. Handlers still execute synchronously; tools that honor // context (custom tools, delegate RPC/A2A, and go-micro RPC clients) return // promptly with a bounded error result when the deadline expires. func (a *agentImpl) toolTimeoutWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { if a.opts.ToolTimeout <= 0 { return next(ctx, call) } toolCtx, cancel := context.WithTimeout(ctx, a.opts.ToolTimeout) defer cancel() return next(toolCtx, call) } } // x402PayWrap pays an x402 Payment Required tool result and retries the // underlying HTTP tool once. Tools that proxy HTTP paid resources can return the // raw x402 402 challenge body and include a "url" input; the agent then uses // wrapper/x402.Client so payer and budget semantics stay in one place. func (a *agentImpl) x402PayWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { res := next(ctx, call) if res.Refused != "" || !isX402Challenge(res.Content) { return res } url, _ := call.Input["url"].(string) if url == "" { return errResult(call.ID, "x402: payment required but tool result did not include a retryable url input") } budget := a.opts.Budget if budget > 0 { remaining := budget - a.spend if remaining <= 0 { return refused(call.ID, ai.RefusedSpendBudget, fmt.Sprintf( "x402 spend budget exceeded: no budget remaining for %s (spent %d of %d)", call.Name, a.spend, budget)) } budget = remaining } client := &x402.Client{Payer: a.opts.Payer, Budget: budget} req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) if err != nil { return errResult(call.ID, err.Error()) } resp, err := client.Do(req) if err != nil { if strings.Contains(err.Error(), "would exceed budget") { return refused(call.ID, ai.RefusedSpendBudget, err.Error()) } return errResult(call.ID, err.Error()) } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return errResult(call.ID, err.Error()) } a.spend += client.Spent() var value any if err := json.Unmarshal(body, &value); err != nil { value = string(body) } return ai.ToolResult{ID: call.ID, Value: value, Content: string(body), Attempts: 2} } } func isX402Challenge(content string) bool { var ch struct { X402Version int `json:"x402Version"` Accepts []x402.Requirements `json:"accepts"` } return json.Unmarshal([]byte(content), &ch) == nil && ch.X402Version > 0 && len(ch.Accepts) > 0 } // toolRetryWrap retries transient tool failures with bounded backoff. It is // opt-in because tools can have side effects; guardrail refusals and caller // cancellation are never retried. func (a *agentImpl) toolRetryWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { maxAttempts := a.opts.ToolMaxAttempts if maxAttempts <= 0 { maxAttempts = 1 } var res ai.ToolResult for attempt := 1; attempt <= maxAttempts; attempt++ { if err := ctx.Err(); err != nil { return errResult(call.ID, err.Error()) } res = next(ctx, call) if !retryableToolResult(res) || attempt == maxAttempts || ctx.Err() != nil { return annotateToolAttempts(res, attempt) } t := time.NewTimer(toolRetryBackoff(attempt, a.opts.ToolRetryBackoff)) select { case <-ctx.Done(): if !t.Stop() { <-t.C } return errResult(call.ID, ctx.Err().Error()) case <-t.C: } } return annotateToolAttempts(res, maxAttempts) } } func retryableToolResult(res ai.ToolResult) bool { if res.Refused != "" { return false } msg := toolErrorMessage(res) if msg == "" { return false } return ai.IsTransientError(fmt.Errorf("%s", msg)) } func toolErrorMessage(res ai.ToolResult) string { if m, ok := res.Value.(map[string]string); ok { return m["error"] } if m, ok := res.Value.(map[string]any); ok { if v, ok := m["error"].(string); ok { return v } } var decoded map[string]string if err := json.Unmarshal([]byte(res.Content), &decoded); err == nil { return decoded["error"] } return "" } func annotateToolAttempts(res ai.ToolResult, attempts int) ai.ToolResult { if attempts <= 1 { return res } res.Attempts = attempts if m, ok := res.Value.(map[string]string); ok { cp := map[string]any{} for k, v := range m { cp[k] = v } cp["attempts"] = attempts res.Value = cp if b, err := json.Marshal(cp); err == nil { res.Content = string(b) } } return res } func toolRetryBackoff(attempt int, base time.Duration) time.Duration { if base <= 0 { base = 200 * time.Millisecond } if shift := attempt - 1; shift > 0 { base <<= shift } if base > 30*time.Second { return 30 * time.Second } return base } // baseHandler executes a tool call: a developer custom tool, the built-in // delegate, or an RPC to the service. It is the innermost handler. func (a *agentImpl) baseHandler() ai.ToolHandler { rpc := a.tools.Handler() return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { for i := range a.opts.tools { if a.opts.tools[i].def.Name == call.Name { out, err := a.opts.tools[i].handler(ctx, call.Input) if err != nil { return errResult(call.ID, err.Error()) } return ai.ToolResult{ID: call.ID, Value: out, Content: out} } } if call.Name == toolHumanInput { return a.handleHumanInput(call) } if call.Name == toolDelegate { return a.handleDelegate(ctx, call) } return rpc(ctx, call) } } // planWrap handles the plan tool inline. plan is internal bookkeeping, // not an action — it is never counted, loop-checked, or gated. func (a *agentImpl) planWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { if call.Name == toolPlan { return a.handlePlan(call) } if containsNestedTextToolCall(call.Input) { return refused(call.ID, ai.RefusedApproval, "malformed tool call: nested text tool-call markup found inside arguments; call the intended tool directly with clean JSON arguments") } if call.Name == toolDelegate { if blocked := a.unfinishedPlanStepsBeforeDelegation(); len(blocked) > 0 { return refused(call.ID, ai.RefusedApproval, "complete these plan steps before delegating: "+strings.Join(blocked, ", ")) } } res := next(ctx, call) if res.Refused == "" && toolErrorMessage(res) == "" { a.completeNextPlanStep() } return res } } // stepWrap bounds the number of actions per Ask (MaxSteps). func (a *agentImpl) stepWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { if a.opts.MaxSteps > 0 { a.steps++ if a.steps > a.opts.MaxSteps { return refused(call.ID, ai.RefusedMaxSteps, fmt.Sprintf( "step limit reached (%d). Do not call any more tools; stop and summarize what you have so far.", a.opts.MaxSteps)) } } return next(ctx, call) } } // loopWrap stops the agent repeating an identical action that makes no // progress (which the step count alone won't catch). func (a *agentImpl) loopWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { if a.opts.LoopLimit > 0 { if a.calls == nil { a.calls = map[string]int{} } args, _ := json.Marshal(call.Input) fp := call.Name + ":" + string(args) a.calls[fp]++ if a.calls[fp] > a.opts.LoopLimit { return refused(call.ID, ai.RefusedLoop, fmt.Sprintf( "loop detected: you have already called %q with the same arguments %d times and the result will not change. Stop repeating it — try a different approach, or finish with what you have.", call.Name, a.opts.LoopLimit)) } } return next(ctx, call) } } // approveWrap gates each action before it runs (ApproveTool). type approvalPause struct { Tool string Message string } type inputPause struct { OriginalMessage string `json:"original_message"` Prompt string `json:"prompt"` } func (a *agentImpl) approveWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { if a.opts.Approve != nil { if ok, reason := a.opts.Approve(call.Name, call.Input); !ok { msg := "tool call was not approved" if reason != "" { msg += ": " + reason } a.pause = &approvalPause{Tool: call.Name, Message: msg} return refused(call.ID, ai.RefusedApproval, msg) } } return next(ctx, call) } } // spendWrap reserves a per-run x402 spend budget before paid tool execution. func (a *agentImpl) spendWrap(next ai.ToolHandler) ai.ToolHandler { return func(ctx context.Context, call ai.ToolCall) ai.ToolResult { amount := a.opts.ToolSpend[call.Name] if amount <= 0 || a.opts.MaxSpend <= 0 { return next(ctx, call) } if a.spend+amount > a.opts.MaxSpend { return refused(call.ID, ai.RefusedSpendBudget, fmt.Sprintf( "x402 spend budget exceeded: paying %d for %s would exceed per-run budget (spent %d of %d)", amount, call.Name, a.spend, a.opts.MaxSpend)) } a.spend += amount if info, ok := ai.RunInfoFrom(ctx); ok { info.Spent = a.spend info.ToolSpend = amount ctx = ai.WithRunInfo(ctx, info) } res := next(ctx, call) if res.Refused != "" || toolErrorMessage(res) != "" { a.spend -= amount } return res } } // handlePlan persists the supplied plan to the agent's memory and // echoes it back so the model can see the stored state. func (a *agentImpl) handlePlan(call ai.ToolCall) ai.ToolResult { input := preserveCompletedPlanSteps(a.loadPlan(), call.Input) data, err := json.Marshal(input) if err != nil { return errResult(call.ID, "invalid plan: "+err.Error()) } _ = a.stateStore().Write(&store.Record{Key: planKey, Value: data}) return ai.ToolResult{ID: call.ID, Value: input, Content: string(data)} } func preserveCompletedPlanSteps(stored string, input map[string]any) map[string]any { if stored == "" { return input } var previous map[string]any if err := json.Unmarshal([]byte(stored), &previous); err != nil { return input } completed := completedPlanTasks(previous) if len(completed) == 0 { return input } steps, ok := input["steps"].([]any) if !ok { return input } for _, raw := range steps { step, ok := raw.(map[string]any) if !ok { continue } task, _ := step["task"].(string) if completed[planTaskCompletionKey(task)] && isUnfinishedPlanStatus(step["status"]) { step["status"] = "done" } } return input } func completedPlanTasks(plan map[string]any) map[string]bool { steps, ok := plan["steps"].([]any) if !ok { return nil } completed := map[string]bool{} for _, raw := range steps { step, ok := raw.(map[string]any) if !ok { continue } status, _ := step["status"].(string) if status != "done" { continue } task, _ := step["task"].(string) if task = planTaskCompletionKey(task); task != "" { completed[task] = true } } return completed } func normalizePlanTask(task string) string { return strings.Join(strings.Fields(strings.ToLower(task)), " ") } func planTaskCompletionKey(task string) string { normalized := normalizePlanTask(task) if normalized == "" { return "" } if isLaunchReadinessDelegationPlanTask(normalized) { return "launch-readiness-notification" } return normalized } func isLaunchReadinessDelegationPlanTask(task string) bool { task = normalizePlanTask(task) if !strings.Contains(task, "notify") && !strings.Contains(task, "notification") { return false } hasLaunchReadiness := strings.Contains(task, "launch") || strings.Contains(task, "readiness") || strings.Contains(task, "ready") hasOwnerComms := strings.Contains(task, "owner") && strings.Contains(task, "comms") return hasLaunchReadiness || hasOwnerComms } func isUnfinishedPlanStatus(status any) bool { s, _ := status.(string) return s == "" || s == "pending" || s == "in_progress" } func (a *agentImpl) completeNextPlanStep() { plan := a.loadPlan() if plan == "" { return } var data map[string]any if err := json.Unmarshal([]byte(plan), &data); err != nil { return } steps, ok := data["steps"].([]any) if !ok { return } for _, raw := range steps { step, ok := raw.(map[string]any) if !ok { continue } status, _ := step["status"].(string) if status == "" || status == "pending" || status == "in_progress" { step["status"] = "done" b, err := json.Marshal(data) if err == nil { _ = a.stateStore().Write(&store.Record{Key: planKey, Value: b}) } return } } } func (a *agentImpl) unfinishedPlanStepsBeforeDelegation() []string { plan := a.loadPlan() if plan == "" { return nil } var data map[string]any if err := json.Unmarshal([]byte(plan), &data); err != nil { return nil } steps, ok := data["steps"].([]any) if !ok { return nil } var unfinished []string for _, raw := range steps { step, ok := raw.(map[string]any) if !ok { continue } task := planStepTask(step) if isDelegationPlanTask(task) { break } if !isUnfinishedPlanStatus(step["status"]) { continue } if task == "" { task = "" } unfinished = append(unfinished, task) } return unfinished } func planStepTask(step map[string]any) string { if task, _ := step["task"].(string); task != "" { return task } desc, _ := step["description"].(string) return desc } func isDelegationPlanTask(task string) bool { task = normalizePlanTask(task) return strings.Contains(task, "delegate") || strings.Contains(task, "notify") || strings.Contains(task, "notification") } func (a *agentImpl) unfinishedPlanSteps() []string { plan := a.loadPlan() if plan == "" { return nil } var data map[string]any if err := json.Unmarshal([]byte(plan), &data); err != nil { return nil } steps, ok := data["steps"].([]any) if !ok { return nil } var unfinished []string for _, raw := range steps { step, ok := raw.(map[string]any) if !ok { continue } status, _ := step["status"].(string) if status != "" && status != "pending" && status != "in_progress" { continue } task := planStepTask(step) if task == "" { task = "" } unfinished = append(unfinished, task) } return unfinished } // handleHumanInput records that the model needs operator input before it can continue. func (a *agentImpl) handleHumanInput(call ai.ToolCall) ai.ToolResult { prompt, _ := call.Input["prompt"].(string) prompt = strings.TrimSpace(prompt) if prompt == "" { prompt = "human input required" } a.pause = &approvalPause{Tool: toolHumanInput, Message: prompt} return refused(call.ID, ai.RefusedApproval, "input-required: "+prompt) } // handleDelegate hands a subtask to another agent. Delegate-first: // if 'to' names a registered agent, it is called via RPC. Otherwise an // ephemeral sub-agent is created with a fresh, isolated context, asked // the subtask, and its reply returned. func (a *agentImpl) handleDelegate(ctx context.Context, call ai.ToolCall) (res ai.ToolResult) { input := call.Input task, _ := input["task"].(string) if task == "" { return errResult(call.ID, "task is required") } to, _ := input["to"].(string) if cached, ok := a.cachedDelegateResult(call.ID, to, task); ok { return cached } key := delegateResultKey(to, task) if cached, ok := a.joinDelegateCall(ctx, call.ID, key); ok { return cached } defer func() { a.finishDelegateCall(key, res) }() // An external agent on another framework, addressed by A2A URL. if strings.HasPrefix(to, "http://") || strings.HasPrefix(to, "https://") { reply, err := a2a.NewClient(to).Send(ctx, task) if err != nil { return errResult(call.ID, "delegate to A2A agent "+to+": "+err.Error()) } return a.storeDelegateResult(call.ID, to, task, map[string]any{"agent": to, "reply": reply}) } // Delegate-first: an existing agent that owns the domain handles it. if to != "" && a.isAgent(to) { reply, err := a.callAgentRPC(ctx, to, task) if err != nil { return errResult(call.ID, "delegate to agent "+to+": "+err.Error()) } return a.storeDelegateResult(call.ID, to, task, map[string]any{"agent": to, "reply": reply}) } // Otherwise create a focused, ephemeral sub-agent. Fresh context: // it loads no history and persists none. var svcs []string if to != "" { svcs = []string{to} } sub := newEphemeral( Name(a.opts.Name+".sub"), Services(svcs...), Prompt("You are a sub-agent handling a single delegated subtask. "+ "Complete it using the available tools and report the result concisely."), Provider(a.opts.Provider), Model(a.opts.Model), APIKey(a.opts.APIKey), WithRegistry(a.opts.Registry), WithClient(a.opts.Client), WithStore(a.opts.Store), ModelCallTimeout(a.opts.ModelTimeout), ModelRetry(a.opts.ModelMaxAttempts, a.opts.ModelRetryBackoff), ToolCallTimeout(a.opts.ToolTimeout), ToolRetry(a.opts.ToolMaxAttempts, a.opts.ToolRetryBackoff), TraceProvider(a.opts.TraceProvider), ) // Record lineage so the sub-agent's tool calls carry this run as parent. sub.parentRunID = a.runID resp, err := sub.Ask(ctx, task) if err != nil { return errResult(call.ID, "sub-agent: "+err.Error()) } return a.storeDelegateResult(call.ID, to, task, map[string]any{"reply": resp.Reply}) } func (a *agentImpl) joinDelegateCall(ctx context.Context, id, key string) (ai.ToolResult, bool) { a.delegateMu.Lock() if a.delegateCalls == nil { a.delegateCalls = map[string]*delegateCall{} } if inFlight := a.delegateCalls[key]; inFlight != nil { a.delegateMu.Unlock() select { case <-ctx.Done(): return errResult(id, ctx.Err().Error()), true case <-inFlight.done: return withToolResultID(inFlight.res, id), true } } a.delegateCalls[key] = &delegateCall{done: make(chan struct{})} a.delegateMu.Unlock() return ai.ToolResult{}, false } func (a *agentImpl) finishDelegateCall(key string, res ai.ToolResult) { a.delegateMu.Lock() inFlight := a.delegateCalls[key] if inFlight == nil { a.delegateMu.Unlock() return } inFlight.res = res delete(a.delegateCalls, key) close(inFlight.done) a.delegateMu.Unlock() } func (a *agentImpl) cachedDelegateResult(id, to, task string) (ai.ToolResult, bool) { recs, err := a.stateStore().Read(delegateResultKey(to, task)) if err != nil || len(recs) == 0 { return ai.ToolResult{}, false } var out map[string]any if err := json.Unmarshal(recs[0].Value, &out); err != nil { return ai.ToolResult{}, false } b, _ := json.Marshal(out) return ai.ToolResult{ID: id, Value: out, Content: string(b)}, true } func (a *agentImpl) storeDelegateResult(id, to, task string, out map[string]any) ai.ToolResult { b, _ := json.Marshal(out) _ = a.stateStore().Write(&store.Record{Key: delegateResultKey(to, task), Value: b}) return ai.ToolResult{ID: id, Value: out, Content: string(b)} } func withToolResultID(res ai.ToolResult, id string) ai.ToolResult { res.ID = id return res } func delegateResultKey(to, task string) string { fp := normalizeDelegateTarget(to) + "\x00" + normalizeDelegateTask(task) sum := sha256.Sum256([]byte(fp)) return fmt.Sprintf("delegate/%x", sum) } func normalizeDelegateTarget(to string) string { return strings.Join(strings.Fields(strings.ToLower(strings.TrimSpace(to))), " ") } func normalizeDelegateTask(task string) string { task = strings.ToLower(strings.TrimSpace(task)) task = strings.Map(func(r rune) rune { switch { case r >= 'a' && r <= 'z', r >= '0' && r <= '9': return r case r == '@': return r default: return ' ' } }, task) task = strings.Join(strings.Fields(task), " ") if strings.Contains(task, "owner") && strings.Contains(task, "acme") && isLaunchReadinessDelegateTask(task) { return "notify owner@acme.com launch-plan-ready" } return task } func isLaunchReadinessDelegateTask(task string) bool { hasNotify := strings.Contains(task, "notify") || strings.Contains(task, "notification") || strings.Contains(task, "tell") hasLaunch := strings.Contains(task, "launch") hasPlanOrReadiness := strings.Contains(task, "plan") || strings.Contains(task, "readiness") || strings.Contains(task, "ready") hasCompletion := strings.Contains(task, "ready") || strings.Contains(task, "readiness") || strings.Contains(task, "prepared") || strings.Contains(task, "complete") || strings.Contains(task, "finished") || strings.Contains(task, "done") || strings.Contains(task, "sent") return hasNotify && hasLaunch && hasPlanOrReadiness && hasCompletion } // isAgent reports whether name resolves to a registered agent (a // service advertising type=agent in its metadata). func (a *agentImpl) isAgent(name string) bool { if a.opts.Registry == nil { return false } recs, err := a.opts.Registry.GetService(name) if err != nil || len(recs) == 0 { return false } if recs[0].Metadata != nil && recs[0].Metadata["type"] == "agent" { return true } for _, n := range recs[0].Nodes { if n.Metadata != nil && n.Metadata["type"] == "agent" { return true } } return false } // callAgentRPC calls another agent's Agent.Chat endpoint and returns // its reply. func (a *agentImpl) callAgentRPC(ctx context.Context, name, msg string) (string, error) { body, _ := json.Marshal(map[string]string{"message": msg}) req := a.opts.Client.NewRequest(name, "Agent.Chat", &codecBytes.Frame{Data: body}) var rsp codecBytes.Frame if err := a.opts.Client.Call(ctx, req, &rsp); err != nil { return "", err } var out struct { Reply string `json:"reply"` } if err := json.Unmarshal(rsp.Data, &out); err != nil { return "", err } return out.Reply, nil } // planKey is the record key for an agent's plan within its scoped store. const planKey = "plan" // loadPlan returns the stored plan as a JSON string, or "" if none. func (a *agentImpl) loadPlan() string { recs, err := a.stateStore().Read(planKey) if err != nil || len(recs) == 0 { return "" } return string(recs[0].Value) } func errResult(id, msg string) ai.ToolResult { m := map[string]string{"error": msg} b, _ := json.Marshal(m) return ai.ToolResult{ID: id, Value: m, Content: string(b)} } // refused is an error result a guardrail returns, tagged with a structured // reason (ai.Refused*) so a tool wrapper can react to it without parsing // the message. func refused(id, reason, msg string) ai.ToolResult { r := errResult(id, msg) r.Refused = reason return r }