feat: add recursion guard with depth limit and cycle detection
Prevent infinite handoff loops (A->B->A) and unbounded depth chains. HandoffRequest now carries Depth/Visited/MaxDepth fields that are propagated to target agents. Default max depth is 3.
This commit is contained in:
parent
4def7b497e
commit
eb707ff724
2 changed files with 61 additions and 0 deletions
|
|
@ -41,12 +41,18 @@ func FindAgentsByCapability(resolver AgentResolver, capability string) []AgentIn
|
|||
return matches
|
||||
}
|
||||
|
||||
// DefaultMaxHandoffDepth is the maximum handoff chain depth when not configured.
|
||||
const DefaultMaxHandoffDepth = 3
|
||||
|
||||
// HandoffRequest describes a delegation from one agent to another.
|
||||
type HandoffRequest struct {
|
||||
FromAgentID string
|
||||
ToAgentID string
|
||||
Task string
|
||||
Context map[string]string // k-v to write to blackboard before handoff
|
||||
Depth int // current depth level (0 = top-level)
|
||||
Visited []string // agent IDs already in the call chain
|
||||
MaxDepth int // max allowed depth (0 = use DefaultMaxHandoffDepth)
|
||||
}
|
||||
|
||||
// HandoffResult contains the outcome of a handoff execution.
|
||||
|
|
@ -59,7 +65,32 @@ type HandoffResult struct {
|
|||
}
|
||||
|
||||
// ExecuteHandoff delegates a task to a target agent, injecting blackboard context.
|
||||
// It enforces recursion guards: depth limit and cycle detection.
|
||||
func ExecuteHandoff(ctx context.Context, resolver AgentResolver, board *Blackboard, req HandoffRequest, channel, chatID string) *HandoffResult {
|
||||
// Recursion guard: depth limit
|
||||
maxDepth := req.MaxDepth
|
||||
if maxDepth == 0 {
|
||||
maxDepth = DefaultMaxHandoffDepth
|
||||
}
|
||||
if req.Depth >= maxDepth {
|
||||
return &HandoffResult{
|
||||
AgentID: req.ToAgentID,
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("handoff depth limit reached (%d/%d): %v -> %s", req.Depth, maxDepth, req.Visited, req.ToAgentID),
|
||||
}
|
||||
}
|
||||
|
||||
// Recursion guard: cycle detection
|
||||
for _, v := range req.Visited {
|
||||
if v == req.ToAgentID {
|
||||
return &HandoffResult{
|
||||
AgentID: req.ToAgentID,
|
||||
Success: false,
|
||||
Error: fmt.Sprintf("handoff cycle detected: %q already in chain %v", req.ToAgentID, req.Visited),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
target := resolver.GetAgentInfo(req.ToAgentID)
|
||||
if target == nil {
|
||||
return &HandoffResult{
|
||||
|
|
@ -76,6 +107,30 @@ func ExecuteHandoff(ctx context.Context, resolver AgentResolver, board *Blackboa
|
|||
}
|
||||
}
|
||||
|
||||
// Propagate depth and visited to target agent's handoff tool
|
||||
newVisited := make([]string, len(req.Visited)+1)
|
||||
copy(newVisited, req.Visited)
|
||||
newVisited[len(req.Visited)] = req.ToAgentID
|
||||
|
||||
if target.Tools != nil {
|
||||
// Wire session blackboard to target's tools
|
||||
if tool, ok := target.Tools.Get("blackboard"); ok {
|
||||
if ba, ok := tool.(BoardAware); ok {
|
||||
ba.SetBoard(board)
|
||||
}
|
||||
}
|
||||
if tool, ok := target.Tools.Get("handoff"); ok {
|
||||
if ba, ok := tool.(BoardAware); ok {
|
||||
ba.SetBoard(board)
|
||||
}
|
||||
if ht, ok := tool.(*HandoffTool); ok {
|
||||
ht.depth = req.Depth + 1
|
||||
ht.visited = newVisited
|
||||
ht.maxDepth = maxDepth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Build system prompt incorporating agent role, system prompt, and blackboard
|
||||
systemPrompt := buildHandoffSystemPrompt(target, board)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ type HandoffTool struct {
|
|||
fromAgentID string
|
||||
originChannel string
|
||||
originChatID string
|
||||
depth int // current handoff depth (0 = top-level)
|
||||
visited []string // agent IDs already in the call chain
|
||||
maxDepth int // max allowed depth (0 = use DefaultMaxHandoffDepth)
|
||||
}
|
||||
|
||||
// NewHandoffTool creates a handoff tool bound to a specific source agent.
|
||||
|
|
@ -133,6 +136,9 @@ func (t *HandoffTool) Execute(ctx context.Context, args map[string]any) *tools.T
|
|||
ToAgentID: agentID,
|
||||
Task: task,
|
||||
Context: contextMap,
|
||||
Depth: t.depth,
|
||||
Visited: t.visited,
|
||||
MaxDepth: t.maxDepth,
|
||||
}, t.originChannel, t.originChatID)
|
||||
|
||||
if !result.Success {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue