package vip.mate.tool.builtin; import java.util.ArrayDeque; import java.util.Deque; import java.util.Set; /** * Tracks Agent delegation call context to prevent infinite recursion and carry parent session info. *

* Uses a ThreadLocal stack so that nested delegations correctly restore the previous layer's * parentConversationId and childDeniedTools on exit. * Each {@link DelegateAgentTool} delegation calls enter() before and exit() after execution. * * @author MateClaw Team */ public final class DelegationContext { /** * Snapshot of one delegation layer's state. * *

{@code rootConversationId} is the human-facing conversation at the top * of the delegation tree — every layer carries it unchanged so that a * grandchild's progress events can be broadcast to the same stream the user * is watching, rather than to its immediate (machine-only) parent. * {@code currentSubagentId} is the id of the subagent running THIS layer; a * deeper child reads it as its own {@code parentSubagentId} to reconstruct * the spawn tree. */ private record Frame(String parentConversationId, Set childDeniedTools, String rootConversationId, String currentSubagentId) {} private static final ThreadLocal> STACK = ThreadLocal.withInitial(ArrayDeque::new); private DelegationContext() {} /** Current delegation depth (0 = top-level call, not inside any delegation) */ public static int currentDepth() { return STACK.get().size(); } /** Parent conversation ID for event relay (from the current frame) */ public static String parentConversationId() { Frame top = STACK.get().peek(); return top != null ? top.parentConversationId : null; } /** Denied tools set for the child Agent (from the current frame) */ public static Set childDeniedTools() { Frame top = STACK.get().peek(); return top != null && top.childDeniedTools != null ? top.childDeniedTools : Set.of(); } /** Root (human-facing) conversation ID for the whole tree, or null at top level. */ public static String rootConversationId() { Frame top = STACK.get().peek(); return top != null ? top.rootConversationId : null; } /** Subagent id of the layer currently executing, or null at top level. */ public static String currentSubagentId() { Frame top = STACK.get().peek(); return top != null ? top.currentSubagentId : null; } /** Enter the next delegation layer (with parent conversation ID and child tool restrictions) */ public static void enter(String parentConversationId, Set deniedTools) { enter(parentConversationId, deniedTools, null, null); } /** * Enter the next delegation layer carrying the full tree identity so deeper * children can broadcast to the root conversation and tag their parent. */ public static void enter(String parentConversationId, Set deniedTools, String rootConversationId, String currentSubagentId) { STACK.get().push(new Frame(parentConversationId, deniedTools, rootConversationId, currentSubagentId)); } /** Enter the next delegation layer (backward-compatible overload) */ public static void enter() { enter(null, null, null, null); } /** Exit the current delegation layer, restoring the previous layer's context */ public static void exit() { Deque stack = STACK.get(); if (!stack.isEmpty()) { stack.pop(); } // Clean up ThreadLocal entirely when the stack is empty to prevent memory leaks if (stack.isEmpty()) { STACK.remove(); } } }