feat: 添加子智能体消息渲染

This commit is contained in:
Wenjie Zhang 2026-06-03 00:08:39 +08:00
parent 6a5926ba41
commit 206ddd8ff0
11 changed files with 546 additions and 113 deletions

View File

@ -85,7 +85,7 @@ def _preview_text(text: str, limit: int = 500) -> str:
def _tool_result_with_thread_id(child_thread_id: str, content: str) -> str:
return f"子智能体线程 ID: {child_thread_id}\n\n{content}"
return f"> 子智能体线程 ID: {child_thread_id}\n\n---\n\n{content}"
def _new_child_thread_id(

View File

@ -31,7 +31,7 @@ WEB_SEARCH_SYSTEM_PROMPT = """你是「网页检索」子智能体,由主智
输出要求
- 返回一份结构化的摘要资料按主题或要点组织
- 每条关键结论后使用 <cite source="$URL" type="url">$INDEX</cite> 标注引用来源$INDEX 1 开始递增
- 每条关键结论后使用 <cite source="$URL" type="url">$INDEX</cite> 标注引用来源$INDEX 1 开始递增不单独成行直接跟在结论后面
- 在结尾汇总参考来源列表逐条列出标题与 URL
- 不要编造来源或链接无法验证的信息要明确标注"""
ACCESS_LEVELS = {"global", "department", "user"}

View File

@ -67,7 +67,8 @@ export const agentApi = {
* @param {string} threadId - 会话ID
* @returns {Promise} - AgentState
*/
getAgentState: (threadId) => apiGet(`/api/chat/thread/${threadId}/state`),
getAgentState: (threadId, { includeMessages = false } = {}) =>
apiGet(`/api/chat/thread/${threadId}/state${includeMessages ? '?include_messages=true' : ''}`),
/**
* Submit feedback for a message

View File

@ -288,13 +288,15 @@
<section class="state-section">
<div class="state-section-header">
<span class="state-section-title">子智能体</span>
<span class="state-section-meta">{{ currentSubagentRuns.length }}</span>
<span class="state-section-meta">{{ displaySubagentRuns.length }}</span>
</div>
<div v-if="currentSubagentRuns.length" class="state-list">
<div v-if="displaySubagentRuns.length" class="state-list">
<div
v-for="(run, index) in currentSubagentRuns"
v-for="(run, index) in displaySubagentRuns"
:key="run.id || `${run.subagent_type || 'subagent'}-${index}`"
class="state-list-item"
:class="{ 'is-clickable': run.child_thread_id }"
@click="run.child_thread_id && openSubagentThread(run)"
>
<img
v-if="getSubagentIconSrc(run)"
@ -333,6 +335,12 @@
</div>
</div>
</div>
<SubagentThreadModal
v-model:open="subagentThreadModal.open"
:child-thread-id="subagentThreadModal.childThreadId"
:subagent-name="subagentThreadModal.subagentName"
/>
</div>
</template>
@ -344,6 +352,7 @@ import {
watch,
nextTick,
computed,
provide,
onUnmounted,
onActivated,
onDeactivated
@ -383,7 +392,10 @@ import { useAgentMentionConfig } from '@/composables/useAgentMentionConfig'
import AgentArtifactsCard from '@/components/AgentArtifactsCard.vue'
import AgentPanel from '@/components/AgentPanel.vue'
import AttachmentTmpUploadModal from '@/components/AttachmentTmpUploadModal.vue'
import { enrichTaskToolCalls } from '@/components/ToolCallingResult/toolRegistry'
import SubagentThreadModal from '@/components/SubagentThreadModal.vue'
import { enrichTaskToolCalls, parseToolCallArgs } from '@/components/ToolCallingResult/toolRegistry'
import { getConversationDisplayItems } from '@/utils/messageGrouping'
import { makeChildThreadId } from '@/utils/subagentThread'
// ==================== PROPS & EMITS ====================
const props = defineProps({
@ -422,8 +434,17 @@ const randomGreeting = greetingMessages[Math.floor(Math.random() * greetingMessa
const chatState = reactive({
currentThreadId: null,
// threadId线
threadStates: {}
threadStates: {},
// task id child_thread_id id
subagentThreadByToolCall: {}
})
const recordSubagentThread = (toolCallId, childThreadId) => {
if (!toolCallId || !childThreadId) return
if (chatState.subagentThreadByToolCall[toolCallId] === childThreadId) return
chatState.subagentThreadByToolCall[toolCallId] = childThreadId
}
const getSubagentThreadIdByToolCall = (toolCallId) =>
(toolCallId && chatState.subagentThreadByToolCall[String(toolCallId)]) || ''
const setCurrentThreadId = (threadId) => {
chatState.currentThreadId = threadId || null
chatThreadsStore.setCurrentThreadId(threadId || null)
@ -728,6 +749,18 @@ const currentSubagentOptionBySlug = computed(() => {
})
return optionBySlug
})
const subagentThreadModal = reactive({
open: false,
childThreadId: '',
subagentName: ''
})
const openSubagentThread = (run) => {
if (!run?.child_thread_id) return
subagentThreadModal.childThreadId = String(run.child_thread_id)
subagentThreadModal.subagentName = getSubagentRunName(run)
subagentThreadModal.open = true
}
const currentStateFiles = computed(() => {
const files = []
const seenPaths = new Set()
@ -770,7 +803,7 @@ const stateSummaryLabel = computed(() => {
totalTodoCount.value +
currentStateFiles.value.length +
currentArtifactFiles.value.length +
currentSubagentRuns.value.length
displaySubagentRuns.value.length
return total ? `${total}` : '暂无内容'
})
@ -828,6 +861,122 @@ const getThreadOngoingMessages = (threadId) => {
const onGoingConvMessages = computed(() => getThreadOngoingMessages(currentChatId.value))
// TaskTool 线 / child_thread_id
provide('getThreadOngoingMessages', getThreadOngoingMessages)
provide('getSubagentThreadIdByToolCall', getSubagentThreadIdByToolCall)
// ongoing task 使
// ongoing task message_delta/tool_call
// hasResult false
const ongoingTaskCalls = computed(() => {
const calls = []
onGoingConvMessages.value.forEach((message, messageIndex) => {
if (message?.type !== 'ai' || !Array.isArray(message.tool_calls)) return
message.tool_calls.forEach((toolCall) => {
const name = toolCall?.name || toolCall?.function?.name
if (name !== 'task') return
const id = toolCall?.id ? String(toolCall.id) : ''
if (!id) return
const args = parseToolCallArgs(toolCall)
calls.push({
id,
messageIndex,
hasResult: Boolean(toolCall.tool_call_result || toolCall.result),
subagentType: args.subagent_type || '',
description: args.description || '',
childThreadId: args.thread_id ? String(args.thread_id) : getSubagentThreadIdByToolCall(id)
})
})
})
return calls
})
// task = task AI
// steer
// child_thread_id
const activeSubagentToolCallIds = computed(() => {
const pending = ongoingTaskCalls.value.filter((call) => !call.hasResult)
if (!pending.length) return new Set()
const lastMessageIndex = pending[pending.length - 1].messageIndex
return new Set(
pending.filter((call) => call.messageIndex === lastMessageIndex).map((call) => call.id)
)
})
provide('activeSubagentToolCallIds', activeSubagentToolCallIds)
// agent_state.subagent_runs task
// steer
const runningSubagentRunsFromStream = computed(() => {
const activeIds = activeSubagentToolCallIds.value
return ongoingTaskCalls.value
.filter((call) => activeIds.has(call.id))
.map((call) => {
const option = call.subagentType ? currentSubagentOptionBySlug.value.get(call.subagentType) : null
return {
id: call.id,
subagent_type: call.subagentType,
subagent_name: option?.name || call.subagentType || '子智能体',
description: call.description,
child_thread_id: call.childThreadId || '',
status: 'running'
}
})
})
// merge_subagent_runs child_thread_id / id 线
// 线/steer
const displaySubagentRuns = computed(() => {
const merged = currentSubagentRuns.value.map((run) => ({ ...run }))
const childIndex = new Map()
const idIndex = new Map()
merged.forEach((run, index) => {
if (run.child_thread_id) childIndex.set(String(run.child_thread_id), index)
if (run.id) idIndex.set(String(run.id), index)
})
runningSubagentRunsFromStream.value.forEach((run) => {
let position
if (run.child_thread_id && childIndex.has(run.child_thread_id)) {
position = childIndex.get(run.child_thread_id)
} else if (idIndex.has(run.id)) {
position = idIndex.get(run.id)
}
if (position === undefined) {
position = merged.length
merged.push(run)
} else {
merged[position] = { ...merged[position], ...run }
}
if (run.child_thread_id) childIndex.set(run.child_thread_id, position)
idIndex.set(run.id, position)
})
return merged
})
// child_thread_id/
watch(
onGoingConvMessages,
(messages) => {
const parentThreadId = currentChatId.value
if (!parentThreadId) return
messages.forEach((message) => {
if (message?.type !== 'ai' || !Array.isArray(message.tool_calls)) return
message.tool_calls.forEach((toolCall) => {
const name = toolCall?.name || toolCall?.function?.name
if (name !== 'task') return
if (toolCall.tool_call_result || toolCall.result) return
const id = toolCall?.id ? String(toolCall.id) : ''
if (!id || chatState.subagentThreadByToolCall[id]) return
const args = parseToolCallArgs(toolCall)
if (args.thread_id || !args.subagent_type) return
makeChildThreadId(parentThreadId, String(args.subagent_type), id).then((childThreadId) => {
recordSubagentThread(id, childThreadId)
})
})
})
},
{ deep: true }
)
const historyConversations = computed(() => {
return MessageProcessor.convertServerHistoryToMessages(currentThreadMessages.value)
})
@ -855,7 +1004,7 @@ const conversationRows = computed(() => {
type: 'conversation',
key: conv.status === 'streaming' ? 'ongoing-conversation' : `history-${index}`,
conv,
displayItems: getConversationDisplayItems(conv)
displayItems: getDisplayItems(conv)
}))
if (currentThreadConfigNotice.value) {
@ -1808,19 +1957,6 @@ const handleResizingChange = (isResizingState, clientX = 0) => {
}
// ==================== HELPER FUNCTIONS ====================
const hasVisibleAssistantBody = (message) => {
if (!message || message.type !== 'ai') return true
const { content, reasoningContent } = MessageProcessor.parseAssistantMessageBody(message)
return Boolean(
content ||
reasoningContent ||
message.error_type ||
message.extra_metadata?.error_type ||
message.isStoppedByUser
)
}
const getMessageToolCalls = (message) => {
return enrichTaskToolCalls(message?.tool_calls, {
subagentRunById: currentSubagentRunById.value,
@ -1829,58 +1965,8 @@ const getMessageToolCalls = (message) => {
})
}
// AI
const getConversationDisplayItems = (conv) => {
if (!Array.isArray(conv?.messages) || conv.messages.length === 0) return []
const items = []
let pendingToolGroup = null
const flushToolGroup = () => {
if (pendingToolGroup && pendingToolGroup.toolCalls.length > 0) {
items.push(pendingToolGroup)
}
pendingToolGroup = null
}
conv.messages.forEach((message, index) => {
if (message.type !== 'ai') {
flushToolGroup()
items.push({
type: 'message',
key: message.id || `message-${index}`,
message,
sourceIndex: index
})
return
}
if (hasVisibleAssistantBody(message)) {
flushToolGroup()
items.push({
type: 'message',
key: message.id || `message-${index}`,
message,
sourceIndex: index
})
}
const toolCalls = getMessageToolCalls(message)
if (toolCalls.length === 0) return
if (!pendingToolGroup) {
pendingToolGroup = {
type: 'tool-group',
key: `tool-group-${message.id || index}`,
toolCalls: []
}
}
pendingToolGroup.toolCalls.push(...toolCalls)
})
flushToolGroup()
return items
}
const getDisplayItems = (conv) =>
getConversationDisplayItems(conv, { enrichToolCalls: getMessageToolCalls })
const isDisplayMessageProcessing = (conv, displayItem) => {
return (
@ -2807,11 +2893,16 @@ watch(currentChatId, (threadId, oldThreadId) => {
cursor: pointer;
}
.state-list-item--button:hover {
.state-list-item--button:hover,
.state-list-item.is-clickable:hover {
border-color: var(--main-200);
background: var(--gray-0);
}
.state-list-item.is-clickable {
cursor: pointer;
}
.state-list-item-icon {
flex-shrink: 0;
font-size: 17px;

View File

@ -0,0 +1,104 @@
<template>
<a-modal
:open="open"
:title="subagentName || '子智能体'"
:footer="null"
:width="800"
:destroyOnClose="true"
@cancel="$emit('update:open', false)"
>
<div class="subagent-thread-modal-body">
<div v-if="loading" class="subagent-thread-modal-state">正在加载子智能体消息...</div>
<div v-else-if="error" class="subagent-thread-modal-state is-error">{{ error }}</div>
<ThreadMessageList v-else :messages="messages" />
</div>
</a-modal>
</template>
<script setup>
import { ref, watch } from 'vue'
import { agentApi } from '@/apis'
import { MessageProcessor } from '@/utils/messageProcessor'
import ThreadMessageList from '@/components/ThreadMessageList.vue'
const props = defineProps({
open: {
type: Boolean,
default: false
},
childThreadId: {
type: String,
default: ''
},
subagentName: {
type: String,
default: ''
}
})
defineEmits(['update:open'])
const loading = ref(false)
const error = ref('')
const messages = ref([])
// LangChain text
const flattenContent = (content) => {
if (typeof content === 'string') return content
if (Array.isArray(content)) {
return content
.filter((block) => block && block.type === 'text')
.map((block) => block.text || '')
.join('')
}
return content ?? ''
}
const loadHistory = async (threadId) => {
if (!threadId) return
loading.value = true
error.value = ''
messages.value = []
try {
// LangGraph checkpoint state tool AI
const response = await agentApi.getAgentState(threadId, { includeMessages: true })
// checkpoint content LangChain MarkdownPreview
const normalized = (response.messages || []).map((msg) => ({
...msg,
content: flattenContent(msg.content)
}))
messages.value = MessageProcessor.convertToolResultToMessages(normalized)
} catch (e) {
error.value = '加载子智能体消息失败'
console.error('Failed to load subagent thread messages:', e)
} finally {
loading.value = false
}
}
watch(
() => [props.open, props.childThreadId],
([isOpen, threadId]) => {
if (isOpen && threadId) loadHistory(threadId)
},
{ immediate: true }
)
</script>
<style lang="less" scoped>
.subagent-thread-modal-body {
max-height: 70vh;
overflow-y: auto;
}
.subagent-thread-modal-state {
padding: 32px 0;
text-align: center;
color: var(--gray-500);
font-size: 13px;
&.is-error {
color: var(--color-error-600);
}
}
</style>

View File

@ -0,0 +1,56 @@
<template>
<div class="thread-message-list">
<template v-for="(conv, convIndex) in conversations" :key="`conv-${convIndex}`">
<template v-for="displayItem in getDisplayItems(conv)" :key="displayItem.key">
<AgentMessageComponent
v-if="displayItem.type === 'message'"
:message="displayItem.message"
:show-refs="false"
:hide-tool-calls="true"
:mention="{}"
/>
<ToolCallsGroupComponent v-else :tool-calls="displayItem.toolCalls" />
</template>
</template>
<div v-if="conversations.length === 0" class="thread-message-list-empty">暂无消息</div>
</div>
</template>
<script setup>
import { computed } from 'vue'
import AgentMessageComponent from '@/components/AgentMessageComponent.vue'
import ToolCallsGroupComponent from '@/components/ToolCallsGroupComponent.vue'
import { MessageProcessor } from '@/utils/messageProcessor'
import { getConversationDisplayItems } from '@/utils/messageGrouping'
const props = defineProps({
messages: {
type: Array,
default: () => []
},
enrichToolCalls: {
type: Function,
default: null
}
})
const conversations = computed(() => MessageProcessor.convertServerHistoryToMessages(props.messages))
const getDisplayItems = (conv) =>
getConversationDisplayItems(conv, props.enrichToolCalls ? { enrichToolCalls: props.enrichToolCalls } : {})
</script>
<style lang="less" scoped>
.thread-message-list {
display: flex;
flex-direction: column;
gap: 8px;
}
.thread-message-list-empty {
padding: 24px 0;
text-align: center;
color: var(--gray-500);
font-size: 13px;
}
</style>

View File

@ -6,11 +6,11 @@
<!-- Header Slot -->
<div class="tool-header" @click="toggleExpand">
<!-- Fixed Status Icon -->
<span v-if="toolCall.status === 'success' || toolCall.tool_call_result">
<span v-if="effectiveStatus === 'completed'">
<component v-if="toolIcon" :is="toolIcon" size="16" class="tool-loader tool-success" />
<CheckCircle v-else size="16" class="tool-loader tool-success" />
</span>
<span v-else-if="toolCall.status === 'error'">
<span v-else-if="effectiveStatus === 'error'">
<XCircle size="16" class="tool-loader tool-error" />
</span>
<span v-else>
@ -71,7 +71,7 @@
</div>
<!-- Result Slot -->
<div class="tool-result" style="opacity: 0.8" v-if="hasResult">
<div class="tool-result" style="opacity: 0.8" v-if="hasResult || forceShowResult">
<slot name="result" :tool-call="toolCall" :result-content="resultContent">
<div class="tool-result-content" :data-tool-call-id="toolCall.id">
<!-- Default rendering -->
@ -112,6 +112,16 @@ const props = defineProps({
appearance: {
type: String,
default: 'card'
},
// 'running' | 'completed' | 'failed' task
status: {
type: String,
default: ''
},
// 使 tool_call_result task result_preview
forceShowResult: {
type: Boolean,
default: false
}
})
@ -125,6 +135,14 @@ const toggleExpand = () => {
isExpanded.value = !isExpanded.value
}
// status tool_call_result/status
const effectiveStatus = computed(() => {
if (props.status) return props.status
if (props.toolCall.status === 'success' || props.toolCall.tool_call_result) return 'completed'
if (props.toolCall.status === 'error') return 'failed'
return 'running'
})
// Tool Name Logic
const toolId = computed(() => getToolCallId(props.toolCall))

View File

@ -1,35 +1,35 @@
<template>
<BaseToolCall :tool-call="toolCall">
<BaseToolCall :tool-call="toolCall" :status="baseStatus" :force-show-result="Boolean(displayResult)">
<template #header>
<div class="sep-header">
<span class="note">{{ subagentDisplayName }}</span>
<span v-if="runStatusLabel" class="run-status" :class="runStatusClass">
{{ runStatusLabel }}
</span>
<span class="separator" v-if="shortDescription">|</span>
<span class="description" v-if="shortDescription">{{ shortDescription }}</span>
<span class="separator" v-if="headerDetail">|</span>
<span class="description" :class="{ 'is-live': isRunning && liveStep }" v-if="headerDetail">
{{ headerDetail }}
</span>
</div>
</template>
<template #params>
<div v-if="description" class="task-description">{{ description }}</div>
<div v-if="childThreadId" class="task-thread-id">
子智能体线程 ID{{ childThreadId }}
</div>
</template>
<template #result="{ resultContent }">
<template #result>
<div class="task-result">
<MarkdownPreview compact :content="String(resultContent)" class="md-preview-wrapper" />
<MarkdownPreview compact :content="String(displayResult)" class="md-preview-wrapper" />
</div>
</template>
</BaseToolCall>
</template>
<script setup>
import { computed } from 'vue'
import { computed, inject } from 'vue'
import BaseToolCall from '../BaseToolCall.vue'
import MarkdownPreview from '@/components/common/MarkdownPreview.vue'
import { MessageProcessor } from '@/utils/messageProcessor'
const props = defineProps({
toolCall: {
@ -38,6 +38,10 @@ const props = defineProps({
}
})
const getThreadOngoingMessages = inject('getThreadOngoingMessages', null)
const getSubagentThreadIdByToolCall = inject('getSubagentThreadIdByToolCall', null)
const activeSubagentToolCallIds = inject('activeSubagentToolCallIds', null)
const parsedArgs = computed(() => {
const args = props.toolCall.args || props.toolCall.function?.arguments
if (!args) return {}
@ -56,12 +60,22 @@ const subagentDisplayName = computed(
const description = computed(
() => parsedArgs.value.description || subagentRun.value?.description || ''
)
const childThreadId = computed(() => subagentRun.value?.child_thread_id || parsedArgs.value.thread_id || '')
const childThreadId = computed(
() =>
subagentRun.value?.child_thread_id ||
parsedArgs.value.thread_id ||
(getSubagentThreadIdByToolCall ? getSubagentThreadIdByToolCall(props.toolCall.id) : '') ||
''
)
const hasToolResult = computed(() => Boolean(props.toolCall.tool_call_result || props.toolCall.result))
// 线 steer
const isActiveRun = computed(() => Boolean(activeSubagentToolCallIds?.value?.has(String(props.toolCall.id))))
const runStatus = computed(() => {
if (props.toolCall.status === 'error') return 'failed'
if (props.toolCall.status !== 'success' && !hasToolResult.value) return 'running'
if (subagentRun.value?.status) return subagentRun.value.status
// ongoing /
// steer
if (hasToolResult.value) return subagentRun.value?.status === 'failed' ? 'failed' : 'completed'
if (isActiveRun.value) return 'running'
return 'completed'
})
const runStatusLabel = computed(() => {
@ -75,11 +89,65 @@ const runStatusClass = computed(() => ({
'is-completed': runStatus.value === 'completed',
'is-failed': runStatus.value === 'failed'
}))
// BaseToolCall failed error
const baseStatus = computed(() => (runStatus.value === 'failed' ? 'error' : runStatus.value))
// ongoing task 退 subagent_run.result_preview id
// steer agent_state reducer
const displayResult = computed(() => {
const toolResult = props.toolCall.tool_call_result?.content || props.toolCall.result
if (toolResult) return toolResult
if (
runStatus.value !== 'running' &&
subagentRun.value?.id &&
String(subagentRun.value.id) === String(props.toolCall.id)
) {
return subagentRun.value.result_preview || ''
}
return ''
})
const shortDescription = computed(() => {
const desc = description.value
if (!desc) return ''
return desc.length > 50 ? desc.slice(0, 50) + '...' : desc
})
const isRunning = computed(() => runStatus.value === 'running')
const truncate = (text, limit = 50) => {
const value = String(text || '').replace(/\s+/g, ' ').trim()
if (!value) return ''
return value.length > limit ? value.slice(0, limit) + '...' : value
}
const formatToolCall = (toolCall) => {
const name = toolCall?.name || toolCall?.function?.name || 'tool'
const rawArgs = toolCall?.args ?? toolCall?.function?.arguments
const args = rawArgs && typeof rawArgs === 'object' ? JSON.stringify(rawArgs) : String(rawArgs ?? '')
return `Call(${name}): ${args}`
}
// 线 ongoing
const liveStep = computed(() => {
if (!isRunning.value || !childThreadId.value || typeof getThreadOngoingMessages !== 'function') {
return ''
}
const messages = getThreadOngoingMessages(childThreadId.value)
const last = messages[messages.length - 1]
if (!last) return ''
const toolCalls = last.tool_calls
if (Array.isArray(toolCalls) && toolCalls.length) {
return truncate(formatToolCall(toolCalls[toolCalls.length - 1]), 80)
}
const { content, reasoningContent } = MessageProcessor.parseAssistantMessageBody(last)
const body = content || reasoningContent
if (body) return truncate(body)
return ''
})
const headerDetail = computed(() => {
if (isRunning.value && liveStep.value) return liveStep.value
return shortDescription.value
})
</script>
<style lang="less" scoped>
@ -116,6 +184,16 @@ const shortDescription = computed(() => {
}
}
.sep-header .description {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
&.is-live {
color: var(--color-primary-700);
}
}
.task-description {
border-radius: 8px;
font-size: 13px;
@ -124,15 +202,11 @@ const shortDescription = computed(() => {
background: var(--gray-50);
}
.task-thread-id {
margin-top: 6px;
color: var(--gray-500);
font-size: 12px;
word-break: break-all;
}
.task-result {
border-radius: 8px;
padding: 0 12px;
max-height: min(600px, 40vh);
overflow: auto;
.md-preview-wrapper {
color: var(--gray-800);

View File

@ -39,11 +39,23 @@
</template>
<script setup>
import { computed, ref, watch } from 'vue'
import { computed, ref, watch, inject } from 'vue'
import { ChevronDown, ChevronRight, Wrench } from 'lucide-vue-next'
import { ToolCallRenderer } from '@/components/ToolCallingResult'
import { getToolCallId, normalizeToolCalls } from '@/components/ToolCallingResult/toolRegistry'
const activeSubagentToolCallIds = inject('activeSubagentToolCallIds', null)
// task tool_call_result task
const toolRunState = (toolCall) => {
if (toolCall.status === 'error') return 'error'
if (toolCall.tool_call_result || toolCall.status === 'success') return 'completed'
if (getToolCallId(toolCall) === 'task') {
return activeSubagentToolCallIds?.value?.has(String(toolCall.id)) ? 'running' : 'completed'
}
return 'running'
}
const props = defineProps({
toolCalls: {
type: Array,
@ -110,16 +122,10 @@ const toolCallsNamesMeta = computed(() => {
})
const statusSummary = computed(() => {
const successCount = normalizedToolCalls.value.filter(
(toolCall) => toolCall.status === 'success' || toolCall.tool_call_result
).length
const runningCount = normalizedToolCalls.value.filter(
(toolCall) =>
toolCall.status !== 'success' && toolCall.status !== 'error' && !toolCall.tool_call_result
).length
const errorCount = normalizedToolCalls.value.filter(
(toolCall) => toolCall.status === 'error'
).length
const states = normalizedToolCalls.value.map(toolRunState)
const successCount = states.filter((state) => state === 'completed').length
const runningCount = states.filter((state) => state === 'running').length
const errorCount = states.filter((state) => state === 'error').length
const parts = []
if (successCount > 0 && successCount === normalizedToolCalls.value.length) {
@ -140,7 +146,6 @@ const toggleToolCallsExpanded = () => {
<style lang="less" scoped>
.tool-calls-container {
width: 100%;
margin: 10px 0;
padding: 0;
.tool-calls-summary {
@ -222,9 +227,8 @@ const toggleToolCallsExpanded = () => {
}
.tool-calls-panel {
padding: 4px 0 4px 12px;
border-left: 1px solid var(--gray-100);
margin-left: 8px;
border-top: 1px solid var(--gray-100);
padding-top: 4px;
margin-top: 4px;
margin-bottom: 8px;
}

View File

@ -0,0 +1,70 @@
import MessageProcessor from '@/utils/messageProcessor'
import { enrichTaskToolCalls } from '@/components/ToolCallingResult/toolRegistry'
const hasVisibleAssistantBody = (message) => {
if (!message || message.type !== 'ai') return true
const { content, reasoningContent } = MessageProcessor.parseAssistantMessageBody(message)
return Boolean(
content ||
reasoningContent ||
message.error_type ||
message.extra_metadata?.error_type ||
message.isStoppedByUser
)
}
const defaultEnrichToolCalls = (message) => enrichTaskToolCalls(message?.tool_calls)
// 将 AI 消息拆成“正文块”和“工具块”,再跨消息合并相邻工具块。
export const getConversationDisplayItems = (conv, { enrichToolCalls = defaultEnrichToolCalls } = {}) => {
if (!Array.isArray(conv?.messages) || conv.messages.length === 0) return []
const items = []
let pendingToolGroup = null
const flushToolGroup = () => {
if (pendingToolGroup && pendingToolGroup.toolCalls.length > 0) {
items.push(pendingToolGroup)
}
pendingToolGroup = null
}
conv.messages.forEach((message, index) => {
if (message.type !== 'ai') {
flushToolGroup()
items.push({
type: 'message',
key: message.id || `message-${index}`,
message,
sourceIndex: index
})
return
}
if (hasVisibleAssistantBody(message)) {
flushToolGroup()
items.push({
type: 'message',
key: message.id || `message-${index}`,
message,
sourceIndex: index
})
}
const toolCalls = enrichToolCalls(message)
if (toolCalls.length === 0) return
if (!pendingToolGroup) {
pendingToolGroup = {
type: 'tool-group',
key: `tool-group-${message.id || index}`,
toolCalls: []
}
}
pendingToolGroup.toolCalls.push(...toolCalls)
})
flushToolGroup()
return items
}

View File

@ -0,0 +1,15 @@
// 复刻后端 yuxi/utils/subagent_thread_utils.py 的 make_child_thread_id
// child_thread_id = "subagent_" + sha256("{parent}:{slug}:{tool_call_id}")[:55]
// 子智能体由 graph.ainvoke 独立调用,流式事件不带 tool_call_id前端据此自行推算 child_thread_id。
const PREFIX = 'subagent_'
const DIGEST_LENGTH = 64 - PREFIX.length
export async function makeChildThreadId(parentThreadId, agentSlug, toolCallId) {
if (!parentThreadId || !agentSlug || !toolCallId) return ''
const data = new TextEncoder().encode(`${parentThreadId}:${agentSlug}:${toolCallId}`)
const buffer = await crypto.subtle.digest('SHA-256', data)
const hex = Array.from(new Uint8Array(buffer))
.map((byte) => byte.toString(16).padStart(2, '0'))
.join('')
return `${PREFIX}${hex.slice(0, DIGEST_LENGTH)}`
}