diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/SessionListTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/SessionListTool.java
new file mode 100644
index 00000000..6ceba1db
--- /dev/null
+++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/SessionListTool.java
@@ -0,0 +1,103 @@
+package vip.mate.tool.builtin;
+
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.ai.tool.annotation.Tool;
+import org.springframework.lang.Nullable;
+import org.springframework.ai.chat.model.ToolContext;
+import org.springframework.stereotype.Component;
+import vip.mate.agent.delegation.SubagentRegistry;
+import vip.mate.agent.delegation.SubagentRegistry.SubagentRecord;
+
+import java.util.List;
+
+/**
+ * Read-only session listing tool: enumerates the live sub-agents spawned from
+ * the current conversation's delegation tree.
+ *
+ *
Completes the spawn / send / list triad alongside {@link DelegateAgentTool}
+ * (spawn). Where {@code delegateParallel} fans children out and blocks for their
+ * combined result, this lets the parent agent inspect the tree mid-reasoning —
+ * which children are still running, what phase / tool each is on, how many tool
+ * calls each has made — so it can decide whether to wait, follow up, or move on
+ * instead of re-dispatching roles it already spawned.
+ *
+ *
Resolves the human-facing root conversation the same way the delegation
+ * relay does ({@link DelegationContext#rootConversationId()} when running inside
+ * a delegated layer, otherwise the current {@link ToolExecutionContext}
+ * conversation), then reads the in-memory {@link SubagentRegistry}. It never
+ * mutates state, so it is safe for children to call as well.
+ *
+ * @author MateClaw Team
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class SessionListTool {
+
+ private final SubagentRegistry subagentRegistry;
+
+ @Tool(description = """
+ List the live sub-agents spawned from the current conversation, including each one's
+ id, target agent, tree depth, status (running/completed/interrupted/stale/timeout),
+ current phase, tool-call count, elapsed time, and goal. Read-only: use it to check on
+ children you delegated before deciding to wait, follow up, or proceed — do NOT re-spawn
+ a role that is already listed as running.""")
+ public String listSubagents(@Nullable ToolContext ctx) {
+ String rootConversationId = resolveRootConversationId();
+ if (rootConversationId == null || rootConversationId.isBlank()) {
+ return "No active sub-agents (no conversation context).";
+ }
+
+ List records = subagentRegistry.snapshotTree(rootConversationId);
+ if (records.isEmpty()) {
+ return "No active sub-agents for this conversation.";
+ }
+
+ long now = System.currentTimeMillis();
+ StringBuilder sb = new StringBuilder();
+ sb.append("Active sub-agents (").append(records.size()).append("):\n");
+ // Stable, human-readable order: shallow layers first, then by spawn time
+ // so a parent reads its direct children before their descendants.
+ records.stream()
+ .sorted((a, b) -> {
+ int byDepth = Integer.compare(a.depth(), b.depth());
+ return byDepth != 0 ? byDepth : Long.compare(a.startedAt(), b.startedAt());
+ })
+ .forEach(r -> sb.append(formatRecord(r, now)).append('\n'));
+ return sb.toString().stripTrailing();
+ }
+
+ /**
+ * Root of the delegation tree to list: the relay-carried root when this call
+ * happens inside a delegated layer, falling back to the current conversation
+ * (the top-level agent's own conversation, which is the tree root for the
+ * children it spawned).
+ */
+ private String resolveRootConversationId() {
+ String root = DelegationContext.rootConversationId();
+ if (root != null && !root.isBlank()) {
+ return root;
+ }
+ try {
+ return ToolExecutionContext.conversationId();
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private String formatRecord(SubagentRecord r, long now) {
+ long elapsedSec = Math.max(0, (now - r.startedAt()) / 1000);
+ String goal = r.goal() == null ? "" : r.goal();
+ if (goal.length() > 80) {
+ goal = goal.substring(0, 80) + "…";
+ }
+ return "- [" + r.subagentId() + "] agent=" + r.agentId()
+ + " depth=" + r.depth()
+ + " status=" + r.status().get()
+ + " phase=" + r.currentPhase().get()
+ + " tools=" + r.toolCount().get()
+ + " elapsed=" + elapsedSec + "s"
+ + " goal=\"" + goal + "\"";
+ }
+}
diff --git a/mateclaw-server/src/test/java/vip/mate/agent/delegation/SubagentRunContextTest.java b/mateclaw-server/src/test/java/vip/mate/agent/delegation/SubagentRunContextTest.java
index a345ede7..6bcde48a 100644
--- a/mateclaw-server/src/test/java/vip/mate/agent/delegation/SubagentRunContextTest.java
+++ b/mateclaw-server/src/test/java/vip/mate/agent/delegation/SubagentRunContextTest.java
@@ -62,10 +62,10 @@ class SubagentRunContextTest {
}
/**
- * RFC 08 G1 guardrail: a context passed explicitly carries its depth across
- * a fresh executor thread, where a size-based thread-local stack would reset
- * to 1. Reconstructing the layer via {@link DelegationContext#push} on the
- * child thread reproduces the real tree depth.
+ * Guardrail: a context passed explicitly carries its depth across a fresh
+ * executor thread, where a size-based thread-local stack would reset to 1.
+ * Reconstructing the layer via {@link DelegationContext#push} on the child
+ * thread reproduces the real tree depth.
*/
@Test
void explicitContextCarriesDepthAcrossThreadHop() throws Exception {
diff --git a/mateclaw-server/src/test/java/vip/mate/tool/builtin/SessionListToolTest.java b/mateclaw-server/src/test/java/vip/mate/tool/builtin/SessionListToolTest.java
new file mode 100644
index 00000000..2f5e1f33
--- /dev/null
+++ b/mateclaw-server/src/test/java/vip/mate/tool/builtin/SessionListToolTest.java
@@ -0,0 +1,90 @@
+package vip.mate.tool.builtin;
+
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import vip.mate.agent.delegation.SubagentRegistry;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * Unit tests for {@link SessionListTool} — the read-only "list" leg of the
+ * spawn / send / list triad.
+ *
+ * @author MateClaw Team
+ */
+class SessionListToolTest {
+
+ private SubagentRegistry registry;
+ private SessionListTool tool;
+
+ @BeforeEach
+ void setUp() {
+ registry = new SubagentRegistry();
+ tool = new SessionListTool(registry);
+ // Drain any delegation frames a prior test may have leaked on this thread.
+ while (DelegationContext.currentDepth() > 0) {
+ DelegationContext.exit();
+ }
+ ToolExecutionContext.clear();
+ }
+
+ @AfterEach
+ void tearDown() {
+ ToolExecutionContext.clear();
+ while (DelegationContext.currentDepth() > 0) {
+ DelegationContext.exit();
+ }
+ }
+
+ @Test
+ void reportsNoContextWhenConversationUnknown() {
+ // No ToolExecutionContext conversation and no delegation frame.
+ String out = tool.listSubagents(null);
+ assertTrue(out.contains("no conversation context"),
+ "expected a no-context message, got: " + out);
+ }
+
+ @Test
+ void reportsEmptyTreeWhenNoSubagents() {
+ ToolExecutionContext.set("conv-root", "tester");
+ String out = tool.listSubagents(null);
+ assertTrue(out.contains("No active sub-agents for this conversation"),
+ "expected an empty-tree message, got: " + out);
+ }
+
+ @Test
+ void listsActiveSubagentsForCurrentConversation() {
+ ToolExecutionContext.set("conv-root", "tester");
+ registry.register("conv-root", "child-1", 11L, "research the topic", null);
+ registry.register("conv-root", "child-2", 22L, "draft the summary", null);
+ // A subagent under a different root must not leak into this listing.
+ registry.register("other-conv", "child-x", 99L, "unrelated work", null);
+
+ String out = tool.listSubagents(null);
+
+ assertTrue(out.contains("Active sub-agents (2)"), "expected exactly two children, got: " + out);
+ assertTrue(out.contains("agent=11"), out);
+ assertTrue(out.contains("agent=22"), out);
+ assertTrue(out.contains("research the topic"), out);
+ assertTrue(out.contains("status=running"), out);
+ assertFalse(out.contains("agent=99"), "other conversation's subagent leaked: " + out);
+ }
+
+ @Test
+ void prefersDelegationRootOverCurrentConversation() {
+ // Inside a delegated layer, the tree root is the human-facing conversation
+ // carried by the delegation frame, not the child's own conversation.
+ ToolExecutionContext.set("child-conv", "tester");
+ DelegationContext.enter("child-conv", java.util.Set.of(), "conv-root", "sa-1", 1);
+ try {
+ registry.register("conv-root", "child-1", 11L, "research the topic", null);
+ String out = tool.listSubagents(null);
+ assertTrue(out.contains("Active sub-agents (1)"), out);
+ assertTrue(out.contains("agent=11"), out);
+ } finally {
+ DelegationContext.exit();
+ }
+ }
+}