mirror of
https://gitee.com/mateos/mateclaw.git
synced 2026-09-13 03:13:41 +08:00
feat(agent,ui): nested subagent timeline + always-on plan panel
This commit is contained in:
parent
66af70388b
commit
8bd8a02cd0
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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<String> 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<String, Object> 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,
|
||||
|
||||
@ -30,11 +30,14 @@
|
||||
<!-- 消息体 -->
|
||||
<div class="msg-body" :class="`${role}-body`">
|
||||
<div class="msg-bubble" :class="`${role}-bubble`">
|
||||
<!-- Plan-step panel — always rendered at the top of the bubble whenever
|
||||
this turn has a plan, in both the segmented and fallback render
|
||||
paths, so plan-mode progress is never buried in a collapsed panel. -->
|
||||
<PlanStepsPanel v-if="planMeta" :plan="planMeta" :is-generating="isGenerating" />
|
||||
|
||||
<!-- ===== 分段式渲染模式(Claude Code 风格)===== -->
|
||||
<template v-if="useSegmentedView">
|
||||
<div class="segments-view">
|
||||
<!-- 计划步骤面板(始终显示在 segments 之上) -->
|
||||
<PlanStepsPanel v-if="planMeta" :plan="planMeta" :is-generating="isGenerating" />
|
||||
<template v-for="iter in groupedIterations" :key="iter.key">
|
||||
<!-- Iteration interrupted before any output landed — surface a chip
|
||||
so the user knows the agent moved on instead of silently
|
||||
@ -117,9 +120,6 @@
|
||||
|
||||
<Transition name="thinking-slide">
|
||||
<div v-if="executionExpanded" class="execution-content">
|
||||
<!-- Plan 步骤进度 -->
|
||||
<PlanStepsPanel v-if="planMeta" :plan="planMeta" :is-generating="isGenerating" />
|
||||
|
||||
<!-- 工具调用列表 -->
|
||||
<div v-if="toolCallsMeta.length" class="tool-calls">
|
||||
<div
|
||||
@ -139,7 +139,7 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="!toolCallsMeta.length && !planMeta" class="execution-empty">
|
||||
<div v-if="!toolCallsMeta.length" class="execution-empty">
|
||||
{{ currentPhaseName }}...
|
||||
</div>
|
||||
</div>
|
||||
@ -974,8 +974,18 @@ const segments = computed<MessageSegment[]>(() => {
|
||||
return segs
|
||||
})
|
||||
|
||||
/** 是否使用分段模式渲染(有 segments 数据且包含多个分段) */
|
||||
const useSegmentedView = computed(() => segments.value.length > 1)
|
||||
/**
|
||||
* Use segmented rendering when there are multiple segments, OR when the turn
|
||||
* contains a delegation segment. Delegations live in `segments` but not in
|
||||
* `metadata.toolCalls`, so the fallback path (which only reads toolCalls)
|
||||
* renders nothing for them — a single-step plan that delegates to a subagent
|
||||
* would otherwise show the subagent call as completely invisible. Forcing
|
||||
* segmented view here makes delegation surface as a timeline entry.
|
||||
*/
|
||||
const useSegmentedView = computed(() =>
|
||||
segments.value.length > 1 ||
|
||||
segments.value.some(s => s.type === 'tool_call' && (s.toolName || '').startsWith('→'))
|
||||
)
|
||||
|
||||
/**
|
||||
* Group segments by iterationIndex so each ReAct iteration renders as its own
|
||||
@ -1168,8 +1178,9 @@ const executionPhaseLabel = computed(() => {
|
||||
|
||||
const showExecutionPanel = computed(() => {
|
||||
if (role.value !== 'assistant') return false
|
||||
// 审批卡片有独立的渲染区域,但 execution panel 也应该在审批阶段展示上下文
|
||||
return toolCallsMeta.value.length > 0 || !!planMeta.value
|
||||
// The plan-step panel renders top-level outside this execution panel,
|
||||
// so plan presence alone no longer keeps an (otherwise empty) panel open.
|
||||
return toolCallsMeta.value.length > 0
|
||||
|| (isGenerating.value && parsedMetadata.value?.currentPhase)
|
||||
|| !!pendingApproval.value
|
||||
})
|
||||
|
||||
@ -56,11 +56,45 @@ const isRead = computed(() => {
|
||||
const isSuccess = computed(() => props.segment.status === 'completed' && props.segment.toolSuccess !== false)
|
||||
const isError = computed(() => props.segment.status === 'error' || props.segment.toolSuccess === false)
|
||||
const isRunning = computed(() => props.segment.status === 'running')
|
||||
|
||||
// Nested subagent timeline relayed from the child conversation: the child's own
|
||||
// plan checklist + the tools it called. Only present on delegation segments.
|
||||
const childTimeline = computed(() => isDelegation.value ? props.segment.childTimeline : undefined)
|
||||
const childPlan = computed(() => childTimeline.value?.plan)
|
||||
const childTools = computed(() => childTimeline.value?.tools || [])
|
||||
const hasChildActivity = computed(() =>
|
||||
!!childPlan.value || childTools.value.length > 0
|
||||
)
|
||||
|
||||
// The body is expandable when there's any nested detail to show – either the
|
||||
// child's activity timeline or the final result preview.
|
||||
const hasBody = computed(() => hasChildActivity.value || !!props.segment.toolResult)
|
||||
|
||||
function childStepStatus(i: number): 'pending' | 'running' | 'completed' {
|
||||
const plan = childPlan.value
|
||||
if (!plan) return 'pending'
|
||||
const done = plan.stepResults?.[i]
|
||||
if (done?.status === 'completed') return 'completed'
|
||||
if (i === plan.currentStep) return 'running'
|
||||
return 'pending'
|
||||
}
|
||||
|
||||
// Compact progress hint shown in the delegation header (e.g. "2/3" plan steps,
|
||||
// or "4 tools") so collapsed delegations still convey what the subagent did.
|
||||
const childProgress = computed(() => {
|
||||
const plan = childPlan.value
|
||||
if (plan?.steps?.length) {
|
||||
const done = plan.stepResults?.filter(r => r?.status === 'completed').length || 0
|
||||
return `${done}/${plan.steps.length}`
|
||||
}
|
||||
const n = childTools.value.length
|
||||
return n ? `${n} ${n === 1 ? 'tool' : 'tools'}` : ''
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="seg-tool" :class="{ 'is-running': isRunning, 'is-success': isSuccess, 'is-error': isError }">
|
||||
<div class="seg-tool__header" @click="segment.toolResult ? (expanded = !expanded) : null">
|
||||
<div class="seg-tool__header" @click="hasBody ? (expanded = !expanded) : null">
|
||||
<span class="seg-tool__status">
|
||||
<el-icon v-if="isRunning" class="is-loading" :size="13"><Loading /></el-icon>
|
||||
<el-icon v-else-if="isSuccess" :size="13"><Select /></el-icon>
|
||||
@ -72,17 +106,50 @@ const isRunning = computed(() => props.segment.status === 'running')
|
||||
<el-icon v-else :size="12"><Setting /></el-icon>
|
||||
</span>
|
||||
<span class="seg-tool__name">{{ displayName }}</span>
|
||||
<span v-if="isDelegation && childProgress" class="seg-tool__badge">{{ childProgress }}</span>
|
||||
<span v-if="truncatedArgs" class="seg-tool__args">{{ truncatedArgs }}</span>
|
||||
<el-icon
|
||||
v-if="segment.toolResult"
|
||||
v-if="hasBody"
|
||||
class="seg-tool__arrow"
|
||||
:class="{ 'is-open': expanded }"
|
||||
:size="11"
|
||||
><ArrowDown /></el-icon>
|
||||
</div>
|
||||
<Transition name="seg-slide">
|
||||
<div v-if="expanded && segment.toolResult" class="seg-tool__body">
|
||||
<pre>{{ resultPreview }}</pre>
|
||||
<div v-if="expanded && hasBody" class="seg-tool__body">
|
||||
<!-- Nested subagent timeline (delegation segments) -->
|
||||
<div v-if="hasChildActivity" class="seg-child">
|
||||
<!-- The child agent's own plan checklist, if it ran in plan mode -->
|
||||
<div v-if="childPlan" class="seg-child__plan">
|
||||
<div
|
||||
v-for="(step, i) in childPlan.steps"
|
||||
:key="i"
|
||||
class="seg-child__step"
|
||||
:class="`is-${childStepStatus(i)}`"
|
||||
>
|
||||
<el-icon v-if="childStepStatus(i) === 'running'" class="is-loading" :size="11"><Loading /></el-icon>
|
||||
<el-icon v-else-if="childStepStatus(i) === 'completed'" :size="11"><Select /></el-icon>
|
||||
<span v-else class="seg-child__dot"></span>
|
||||
<span class="seg-child__step-text">{{ step }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The tools the child agent called -->
|
||||
<div v-if="childTools.length" class="seg-child__tools">
|
||||
<div
|
||||
v-for="(t, i) in childTools"
|
||||
:key="i"
|
||||
class="seg-child__tool"
|
||||
:class="`is-${t.status}`"
|
||||
>
|
||||
<el-icon v-if="t.status === 'running'" class="is-loading" :size="11"><Loading /></el-icon>
|
||||
<el-icon v-else-if="t.status === 'completed'" :size="11"><Select /></el-icon>
|
||||
<el-icon v-else :size="11"><CloseBold /></el-icon>
|
||||
<span class="seg-child__tool-name">{{ getToolLabel(t.name) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Final tool/agent result preview -->
|
||||
<pre v-if="segment.toolResult">{{ resultPreview }}</pre>
|
||||
</div>
|
||||
</Transition>
|
||||
</div>
|
||||
@ -177,6 +244,51 @@ const isRunning = computed(() => props.segment.status === 'running')
|
||||
.seg-tool__body {
|
||||
padding: 0 10px 6px 22px;
|
||||
}
|
||||
|
||||
.seg-tool__badge {
|
||||
flex-shrink: 0;
|
||||
font-size: 11px;
|
||||
color: var(--mc-text-tertiary);
|
||||
background: var(--mc-bg-muted);
|
||||
border-radius: 8px;
|
||||
padding: 0 6px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
/* Nested subagent timeline */
|
||||
.seg-child {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
.seg-child__plan {
|
||||
margin-bottom: 6px;
|
||||
padding-left: 4px;
|
||||
border-left: 2px solid var(--mc-border-light);
|
||||
}
|
||||
.seg-child__step,
|
||||
.seg-child__tool {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.7;
|
||||
color: var(--mc-text-tertiary);
|
||||
}
|
||||
.seg-child__step.is-running,
|
||||
.seg-child__tool.is-running { color: var(--mc-primary); }
|
||||
.seg-child__step.is-completed,
|
||||
.seg-child__tool.is-completed { color: var(--mc-text-secondary); }
|
||||
.seg-child__tool.is-error { color: var(--mc-danger); }
|
||||
.seg-child__dot {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
border-radius: 50%;
|
||||
background: var(--mc-text-quaternary, #c0c0c0);
|
||||
flex-shrink: 0;
|
||||
margin: 0 3px;
|
||||
}
|
||||
.seg-child__tools {
|
||||
padding-left: 6px;
|
||||
}
|
||||
.seg-tool__body pre {
|
||||
margin: 0;
|
||||
padding: 8px 10px;
|
||||
|
||||
@ -746,8 +746,8 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
||||
|
||||
// ===== Agent event handlers =====
|
||||
|
||||
// Body of tool_call_started — extracted so delegation_batch can replay the
|
||||
// same behavior for buffered child events without duplicating logic.
|
||||
// Body of tool_call_started. Used directly and reused once (split out as a
|
||||
// function ⟶ no logic duplication).
|
||||
function handleToolCallStarted(data: any) {
|
||||
if (isStaleEvent(data)) return
|
||||
streamPhase.value = 'executing_tool'
|
||||
@ -1001,31 +1001,48 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
||||
? rawPayload
|
||||
: (() => { try { return JSON.parse(String(rawPayload || '{}')) } catch { return {} } })()
|
||||
|
||||
if (data.originalEvent === 'tool_call_started') {
|
||||
const toolName = childData?.toolName || ''
|
||||
if (toolName) {
|
||||
delegSeg.toolArgs = (delegSeg.toolArgs || '') + `\n → ${toolName}`
|
||||
// Build a structured child timeline on the delegation segment instead of
|
||||
// jamming tool names into toolArgs as text. The timeline holds the child
|
||||
// agent's own plan checklist + the tools it called, so the UI can render
|
||||
// a proper nested view (see ToolCallSegment.vue delegation branch).
|
||||
const timeline = (delegSeg.childTimeline ||= { tools: [] })
|
||||
if (!timeline.tools) timeline.tools = []
|
||||
|
||||
switch (data.originalEvent) {
|
||||
case 'tool_call_started': {
|
||||
const name = childData?.toolName || ''
|
||||
if (name) timeline.tools.push({ name, status: 'running' })
|
||||
break
|
||||
}
|
||||
} else if (data.originalEvent === 'tool_call_completed') {
|
||||
const toolName = childData?.toolName || ''
|
||||
const success = childData?.success !== false
|
||||
if (toolName) {
|
||||
// Replace the matching "→ toolName" hint with "✓/✗ toolName"
|
||||
delegSeg.toolArgs = (delegSeg.toolArgs || '').replace(
|
||||
new RegExp(`\\n → ${toolName.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`),
|
||||
`\n ${success ? '✓' : '✗'} ${toolName}`)
|
||||
case 'tool_call_completed': {
|
||||
const name = childData?.toolName || ''
|
||||
const ok = childData?.success !== false
|
||||
// Match the most recent running entry with this name.
|
||||
const entry = [...timeline.tools].reverse()
|
||||
.find(t => t.name === name && t.status === 'running')
|
||||
if (entry) entry.status = ok ? 'completed' : 'error'
|
||||
break
|
||||
}
|
||||
} else if (data.originalEvent === 'phase') {
|
||||
const phase = childData?.phase || String(rawPayload || '')
|
||||
const phaseHints: Record<string, string> = {
|
||||
reasoning: '…',
|
||||
executing_tool: '→',
|
||||
planning: '📋',
|
||||
summarizing: '✍',
|
||||
case 'plan_created': {
|
||||
const steps = childData?.steps
|
||||
if (Array.isArray(steps)) {
|
||||
timeline.plan = { planId: childData?.planId ?? '', steps, currentStep: 0, stepResults: [] }
|
||||
}
|
||||
break
|
||||
}
|
||||
const hint = phaseHints[phase]
|
||||
if (hint && !delegSeg.toolArgs?.endsWith(hint)) {
|
||||
delegSeg.toolArgs = (delegSeg.toolArgs || '').trimEnd() + ' ' + hint
|
||||
case 'plan_step_started': {
|
||||
if (timeline.plan && typeof childData?.index === 'number') {
|
||||
timeline.plan.currentStep = childData.index
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'plan_step_completed': {
|
||||
if (timeline.plan && typeof childData?.index === 'number') {
|
||||
const results = [...(timeline.plan.stepResults || [])]
|
||||
results[childData.index] = { result: childData.result ?? '', status: 'completed' }
|
||||
timeline.plan.stepResults = results
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
flushSegmentsToMessage()
|
||||
@ -1106,6 +1123,9 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
||||
if (delegSeg) {
|
||||
delegSeg.status = data.success ? 'completed' : 'error'
|
||||
delegSeg.toolSuccess = data.success
|
||||
if (data.resultPreview) {
|
||||
delegSeg.toolResult = data.resultPreview
|
||||
}
|
||||
if (data.durationMs) {
|
||||
delegSeg.toolArgs = (delegSeg.toolArgs || '').trimEnd() + ` (${Math.round(data.durationMs / 1000)}s)`
|
||||
}
|
||||
@ -1211,26 +1231,9 @@ export function useChat(options: UseChatOptions): UseChatReturn {
|
||||
flushSegmentsToMessage()
|
||||
})
|
||||
|
||||
stream.on('delegation_batch', (data) => {
|
||||
if (isStaleEvent(data)) return
|
||||
// Buffered child events from a delegated subagent. Replay them in order
|
||||
// through the same handlers as live events so segment state stays
|
||||
// consistent with the rest of the timeline.
|
||||
const events = Array.isArray(data?.events) ? data.events : []
|
||||
for (const ev of events) {
|
||||
const evData = ev?.data ?? {}
|
||||
switch (ev?.event) {
|
||||
case 'tool_call_started':
|
||||
handleToolCallStarted(evData)
|
||||
break
|
||||
case 'tool_call_completed':
|
||||
handleToolCallCompleted(evData)
|
||||
break
|
||||
// Other event kinds (phase / thinking_delta / content_delta / etc.)
|
||||
// are not currently produced inside batches; extend here when added.
|
||||
}
|
||||
}
|
||||
})
|
||||
// Delegation batch envelopes are unpacked server-side into individual
|
||||
// delegation_progress events (see DelegateAgentTool.relayBatchEnvelope),
|
||||
// so the frontend only handles delegation_progress — no batch handler needed.
|
||||
|
||||
stream.on('plan_created', (data) => {
|
||||
if (isStaleEvent(data)) return
|
||||
|
||||
@ -67,7 +67,6 @@ export type SSEEventType =
|
||||
| 'iteration_end'
|
||||
| 'content_truncated'
|
||||
| 'tool_result_chunk'
|
||||
| 'delegation_batch'
|
||||
// Recovery affordance for non-transient errors (ERROR_FALLBACK turns)
|
||||
| 'feedback_event'
|
||||
// Context compaction lifecycle. Fired by ConversationWindowManager
|
||||
|
||||
@ -168,6 +168,15 @@ export interface MessageSegment {
|
||||
approval?: PendingApprovalMeta
|
||||
/** type=plan */
|
||||
plan?: PlanMeta
|
||||
/**
|
||||
* For delegation segments (toolName starts with "→"): the subagent's own
|
||||
* activity, relayed from the child conversation. Renders as a nested timeline
|
||||
* (its plan checklist + the tools it called) instead of jammed text in toolArgs.
|
||||
*/
|
||||
childTimeline?: {
|
||||
plan?: PlanMeta
|
||||
tools?: { name: string; status: 'running' | 'completed' | 'error' }[]
|
||||
}
|
||||
/** 时间戳 */
|
||||
timestamp?: number
|
||||
/**
|
||||
|
||||
Loading…
Reference in New Issue
Block a user