From 8bd8a02cd07141b9c76e38385c8cf12e55a53d5d Mon Sep 17 00:00:00 2001 From: matevip Date: Fri, 22 May 2026 09:48:06 +0800 Subject: [PATCH] feat(agent,ui): nested subagent timeline + always-on plan panel --- .../mate/channel/web/ChatStreamTracker.java | 5 + .../mate/tool/builtin/DelegateAgentTool.java | 88 ++++++++++--- .../src/components/chat/MessageBubble.vue | 31 +++-- .../src/components/chat/ToolCallSegment.vue | 120 +++++++++++++++++- mateclaw-ui/src/composables/chat/useChat.ts | 91 ++++++------- mateclaw-ui/src/composables/chat/useStream.ts | 1 - mateclaw-ui/src/types/index.ts | 9 ++ 7 files changed, 267 insertions(+), 78 deletions(-) diff --git a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java index d5eb5a7a..c91c3c10 100644 --- a/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java +++ b/mateclaw-server/src/main/java/vip/mate/channel/web/ChatStreamTracker.java @@ -394,6 +394,11 @@ public class ChatStreamTracker { || "error".equals(eventName) || "tool_approval_requested".equals(eventName) || "phase".equals(eventName) + // Plan lifecycle events from a child agent: flush buffered + // tool calls first so the parent timeline preserves order. + || "plan_created".equals(eventName) + || "plan_step_started".equals(eventName) + || "plan_step_completed".equals(eventName) || "done".equals(eventName); } diff --git a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java index 46124bd2..45fc2b16 100644 --- a/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java +++ b/mateclaw-server/src/main/java/vip/mate/tool/builtin/DelegateAgentTool.java @@ -890,6 +890,11 @@ public class DelegateAgentTool { private ChildResult runSingleChild(int taskIndex, AgentEntity target, String task, String parentConversationId, String childConversationId, ChatOrigin parentOrigin) { + boolean relayChildEvents = parentConversationId != null && streamTracker.isRunning(parentConversationId); + if (relayChildEvents) { + streamTracker.register(childConversationId); + streamTracker.incrementFlux(childConversationId); + } DelegationContext.enter(parentConversationId, deniedToolsForChild()); try { long startTime = System.currentTimeMillis(); @@ -909,6 +914,9 @@ public class DelegateAgentTool { taskIndex, target.getName(), e.getMessage()); return ChildResult.ofError(taskIndex, target.getName(), e.getMessage()); } finally { + if (relayChildEvents) { + streamTracker.complete(childConversationId); + } DelegationContext.exit(); } } @@ -1109,9 +1117,14 @@ public class DelegateAgentTool { return childConvId; } + /** Child event types that are relayed to the parent for the nested delegation timeline. */ + private static final Set RELAYED_CHILD_EVENTS = Set.of( + "tool_call_started", "tool_call_completed", "phase", + "plan_created", "plan_step_started", "plan_step_completed"); + private Runnable registerRelay(String childConvId, String parentConvId, String childAgentName) { return streamTracker.addEventRelay(childConvId, (eventName, jsonData) -> { - if ("tool_call_started".equals(eventName) || "tool_call_completed".equals(eventName) || "phase".equals(eventName)) { + if (RELAYED_CHILD_EVENTS.contains(eventName)) { try { // Parse jsonData into a plain Object so the frontend receives a proper // JSON object under "data", not a string containing serialized JSON. @@ -1149,28 +1162,65 @@ public class DelegateAgentTool { private Runnable registerBatchedRelay(String childConvId, String parentConvId, String childAgentName) { return streamTracker.addBatchedEventRelay(childConvId, parentConvId, 5, 500L, (eventName, jsonData) -> { - if ("tool_call_started".equals(eventName) - || "tool_call_completed".equals(eventName) - || "phase".equals(eventName)) { - try { - Object parsedData; - try { - parsedData = objectMapper.readValue(jsonData, Object.class); - } catch (Exception ignored) { - parsedData = jsonData; - } - streamTracker.broadcastObject(parentConvId, "delegation_progress", Map.of( - "childConversationId", childConvId, - "childAgentName", childAgentName, - "originalEvent", eventName, - "data", parsedData)); - } catch (Exception e) { - log.debug("Batched relay error: {}", e.getMessage()); - } + // The batched relay delivers (1) pass-through events directly + // (plan/phase/error) and (2) batched tool-calls as a + // "delegation_batch" envelope. Unpack each form to a stream + // of delegation_progress events on the parent so the frontend + // only handles a single event shape (see useChat delegation_progress). + if ("delegation_batch".equals(eventName)) { + relayBatchEnvelope(jsonData, childConvId, parentConvId, childAgentName); + } else if (RELAYED_CHILD_EVENTS.contains(eventName)) { + relayChildEvent(eventName, jsonData, childConvId, parentConvId, childAgentName); } }); } + /** Forward one child event to the parent as a delegation_progress envelope. */ + private void relayChildEvent(String eventName, String jsonData, + String childConvId, String parentConvId, String childAgentName) { + try { + Object parsedData; + try { + parsedData = objectMapper.readValue(jsonData, Object.class); + } catch (Exception ignored) { + parsedData = jsonData; + } + streamTracker.broadcastObject(parentConvId, "delegation_progress", Map.of( + "childConversationId", childConvId, + "childAgentName", childAgentName, + "originalEvent", eventName, + "data", parsedData)); + } catch (Exception e) { + log.debug("Child event relay error: {}", e.getMessage()); + } + } + + /** Unpack a delegation_batch envelope and replay each entry as delegation_progress. */ + @SuppressWarnings("unchecked") + private void relayBatchEnvelope(String envelopeJson, String childConvId, + String parentConvId, String childAgentName) { + try { + Map envelope = objectMapper.readValue(envelopeJson, Map.class); + Object eventsObj = envelope.get("events"); + if (!(eventsObj instanceof List events)) return; + for (Object entryObj : events) { + if (!(entryObj instanceof Map entry)) continue; + Object name = entry.get("event"); + Object payload = entry.get("data"); + if (name == null) continue; + if (!RELAYED_CHILD_EVENTS.contains(name.toString())) continue; + String payloadJson = payload == null + ? "{}" + : (payload instanceof String s ? s : objectMapper.writeValueAsString(payload)); + relayChildEvent(name.toString(), + payloadJson, + childConvId, parentConvId, childAgentName); + } + } catch (Exception e) { + log.debug("Batch envelope relay error: {}", e.getMessage()); + } + } + private void broadcastEnd(String parentConvId, String childConvId, String agentName, ChildResult result) { streamTracker.broadcastObject(parentConvId, "delegation_end", Map.of( "childConversationId", childConvId, diff --git a/mateclaw-ui/src/components/chat/MessageBubble.vue b/mateclaw-ui/src/components/chat/MessageBubble.vue index 505f06df..562b71c2 100644 --- a/mateclaw-ui/src/components/chat/MessageBubble.vue +++ b/mateclaw-ui/src/components/chat/MessageBubble.vue @@ -30,11 +30,14 @@
+ + +