refactor: 消息持久化从批量改为实时逐条,通过装饰器模式实现
- 新增 PersistingEmittedMessageHandler 装饰器,在 emitter 广播前逐条落库 - processor 和 task/runtime 使用装饰器包裹 emitter,替代 post-loop 批量写入 - 移除 session_history 中的批量 DB 写入,仅保留内存历史更新 - execution 中跳过已由 live emitter 实时广播的工具消息,避免重复 - 前端支持运行中 task 工具卡片"查看实时进度"跳转子智能体视图 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
parent
3b0b4c1f2e
commit
d0051baa07
@ -2,6 +2,7 @@ use crate::agent::AgentRuntimeConfig;
|
|||||||
use crate::agent::{SystemPromptContext, SystemPromptProvider};
|
use crate::agent::{SystemPromptContext, SystemPromptProvider};
|
||||||
use crate::bus::ChatMessage;
|
use crate::bus::ChatMessage;
|
||||||
use crate::bus::message::ToolMessageState;
|
use crate::bus::message::ToolMessageState;
|
||||||
|
use crate::storage::ConversationRepository;
|
||||||
use crate::domain::messages::{ContentBlock, ToolCall};
|
use crate::domain::messages::{ContentBlock, ToolCall};
|
||||||
use crate::observability::{
|
use crate::observability::{
|
||||||
Observer, ObserverEvent, ToolExecutionOutcome, ToolExecutionState, truncate_args,
|
Observer, ObserverEvent, ToolExecutionOutcome, ToolExecutionState, truncate_args,
|
||||||
@ -657,6 +658,36 @@ pub trait EmittedMessageHandler: Send + Sync + 'static {
|
|||||||
async fn handle(&self, message: ChatMessage);
|
async fn handle(&self, message: ChatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 装饰器:在内部 emitter 广播前,先将消息持久化到 DB
|
||||||
|
pub struct PersistingEmittedMessageHandler<H: EmittedMessageHandler> {
|
||||||
|
inner: H,
|
||||||
|
conversation_repository: Arc<dyn ConversationRepository>,
|
||||||
|
session_id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<H: EmittedMessageHandler> PersistingEmittedMessageHandler<H> {
|
||||||
|
pub fn new(
|
||||||
|
inner: H,
|
||||||
|
conversation_repository: Arc<dyn ConversationRepository>,
|
||||||
|
session_id: impl Into<String>,
|
||||||
|
) -> Self {
|
||||||
|
Self { inner, conversation_repository, session_id: session_id.into() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[async_trait]
|
||||||
|
impl<H: EmittedMessageHandler> EmittedMessageHandler for PersistingEmittedMessageHandler<H> {
|
||||||
|
async fn handle(&self, message: ChatMessage) {
|
||||||
|
if let Err(e) = self.conversation_repository
|
||||||
|
.append_message(&self.session_id, &message)
|
||||||
|
{
|
||||||
|
tracing::error!(error = %e, session_id = %self.session_id,
|
||||||
|
"Failed to persist emitted message");
|
||||||
|
}
|
||||||
|
self.inner.handle(message).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub trait SkillProvider: Send + Sync + 'static {
|
pub trait SkillProvider: Send + Sync + 'static {
|
||||||
fn system_index_prompt(&self) -> Option<String>;
|
fn system_index_prompt(&self) -> Option<String>;
|
||||||
|
|
||||||
|
|||||||
@ -4,7 +4,8 @@ pub mod runtime_config;
|
|||||||
pub mod system_prompt;
|
pub mod system_prompt;
|
||||||
|
|
||||||
pub use agent_loop::{
|
pub use agent_loop::{
|
||||||
AgentError, AgentLoop, AgentProcessResult, EmittedMessageHandler, SkillProvider,
|
AgentError, AgentLoop, AgentProcessResult, EmittedMessageHandler,
|
||||||
|
PersistingEmittedMessageHandler, SkillProvider,
|
||||||
};
|
};
|
||||||
pub use context_compressor::ContextCompressor;
|
pub use context_compressor::ContextCompressor;
|
||||||
pub use runtime_config::AgentRuntimeConfig;
|
pub use runtime_config::AgentRuntimeConfig;
|
||||||
|
|||||||
@ -77,7 +77,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
MessageKind::Notification => {
|
MessageKind::Notification => {
|
||||||
// 根据元数据判断具体类型
|
// 根据元数据判断具体类型
|
||||||
@ -97,7 +97,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(session_id) = response.metadata.get("session_id") {
|
} else if let Some(session_id) = response.metadata.get("session_id") {
|
||||||
@ -136,7 +136,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(sessions_json) = response.metadata.get("sessions") {
|
} else if let Some(sessions_json) = response.metadata.get("sessions") {
|
||||||
@ -154,7 +154,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else if let Some(topics_json) = response.metadata.get("topics") {
|
} else if let Some(topics_json) = response.metadata.get("topics") {
|
||||||
@ -173,7 +173,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -182,7 +182,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -194,7 +194,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
|
|||||||
id: response.request_id.to_string(),
|
id: response.request_id.to_string(),
|
||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: "assistant".to_string(),
|
role: "assistant".to_string(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(), subagent_task_id: None,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
outbounds.push(outbound);
|
outbounds.push(outbound);
|
||||||
|
|||||||
@ -157,7 +157,11 @@ impl AgentExecutionService {
|
|||||||
.emitted_messages
|
.emitted_messages
|
||||||
.iter()
|
.iter()
|
||||||
.filter(|message| {
|
.filter(|message| {
|
||||||
(!message.is_assistant_tool_call_message() || !request.suppress_live_tool_calls)
|
// 当存在 live_emitter 时,工具调用和工具结果已在 loop 中实时广播
|
||||||
|
// 只保留最终 assistant 文本通过 post-loop 路径发送
|
||||||
|
let already_emitted = request.suppress_live_tool_calls
|
||||||
|
&& (message.is_assistant_tool_call_message() || message.role == "tool");
|
||||||
|
!already_emitted
|
||||||
&& should_display_message_to_user(self.show_tool_results, message)
|
&& should_display_message_to_user(self.show_tool_results, message)
|
||||||
})
|
})
|
||||||
.flat_map(|message| {
|
.flat_map(|message| {
|
||||||
|
|||||||
@ -2,7 +2,7 @@ use std::sync::Arc;
|
|||||||
|
|
||||||
use tokio::sync::Semaphore;
|
use tokio::sync::Semaphore;
|
||||||
|
|
||||||
use crate::agent::{AgentError, CompositeSystemPromptProvider};
|
use crate::agent::{AgentError, CompositeSystemPromptProvider, PersistingEmittedMessageHandler};
|
||||||
use crate::bus::{InboundMessage, MessageBus, OutboundMessage};
|
use crate::bus::{InboundMessage, MessageBus, OutboundMessage};
|
||||||
use crate::command::adapter::InputAdapter;
|
use crate::command::adapter::InputAdapter;
|
||||||
use crate::command::adapters::channel::ChannelInputAdapter;
|
use crate::command::adapters::channel::ChannelInputAdapter;
|
||||||
@ -218,11 +218,15 @@ impl InboundProcessor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 普通消息进入 AgentLoop
|
// 普通消息进入 AgentLoop
|
||||||
let live_emitter = Arc::new(BusToolCallEmitter::new(
|
let live_emitter = Arc::new(PersistingEmittedMessageHandler::new(
|
||||||
self.bus.clone(),
|
BusToolCallEmitter::new(
|
||||||
inbound.channel.clone(),
|
self.bus.clone(),
|
||||||
inbound.chat_id.clone(),
|
inbound.channel.clone(),
|
||||||
inbound.forwarded_metadata.clone(),
|
inbound.chat_id.clone(),
|
||||||
|
inbound.forwarded_metadata.clone(),
|
||||||
|
),
|
||||||
|
self.session_manager.store(),
|
||||||
|
&session_id,
|
||||||
));
|
));
|
||||||
|
|
||||||
match self
|
match self
|
||||||
|
|||||||
@ -158,39 +158,23 @@ impl SessionHistory {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_id = self.persistent_session_id(chat_id);
|
|
||||||
let topic_id = self.chat_topic_ids.get(chat_id).map(|s| s.as_str());
|
|
||||||
self.conversations
|
|
||||||
.append_messages_batch(&session_id, topic_id, &messages)
|
|
||||||
.map_err(|err| {
|
|
||||||
AgentError::Other(format!("batch append messages error: {}", err))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
for message in messages {
|
for message in messages {
|
||||||
self.add_message(chat_id, message);
|
self.add_message(chat_id, message);
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 将消息保存到指定话题(直接写入数据库,不更新内存历史)
|
/// 将消息保存到指定话题
|
||||||
/// 用于异步执行结果保存到原始话题的场景
|
/// 每条消息已通过 PersistingEmittedMessageHandler 逐条持久化,此处仅保留接口兼容
|
||||||
pub(crate) fn append_to_topic(
|
pub(crate) fn append_to_topic(
|
||||||
&self,
|
&self,
|
||||||
chat_id: &str,
|
_chat_id: &str,
|
||||||
topic_id: &str,
|
_topic_id: &str,
|
||||||
messages: &[ChatMessage],
|
messages: &[ChatMessage],
|
||||||
) -> Result<(), AgentError> {
|
) -> Result<(), AgentError> {
|
||||||
if messages.is_empty() {
|
if messages.is_empty() {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let session_id = self.persistent_session_id(chat_id);
|
|
||||||
self.conversations
|
|
||||||
.append_messages_batch(&session_id, Some(topic_id), messages)
|
|
||||||
.map_err(|err| {
|
|
||||||
AgentError::Other(format!("batch append to topic error: {}", err))
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -512,6 +512,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: msg.role.clone(),
|
role: msg.role.clone(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
|
subagent_task_id: None,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
"tool" => {
|
"tool" => {
|
||||||
@ -532,6 +533,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: msg.role.clone(),
|
role: msg.role.clone(),
|
||||||
resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(),
|
resume_hint: "完成外部操作后,直接发一条继续消息即可。".to_string(),
|
||||||
|
subagent_task_id: None,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -540,6 +542,7 @@ fn chat_message_to_ws_outbound(msg: &crate::bus::ChatMessage) -> Option<WsOutbou
|
|||||||
content: msg.content.clone(),
|
content: msg.content.clone(),
|
||||||
role: msg.role.clone(),
|
role: msg.role.clone(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
|
subagent_task_id: None,
|
||||||
}),
|
}),
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,6 +79,8 @@ pub enum WsOutbound {
|
|||||||
role: String,
|
role: String,
|
||||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||||
attachments: Vec<MediaSummary>,
|
attachments: Vec<MediaSummary>,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
subagent_task_id: Option<String>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "tool_call")]
|
#[serde(rename = "tool_call")]
|
||||||
ToolCall {
|
ToolCall {
|
||||||
@ -109,6 +111,8 @@ pub enum WsOutbound {
|
|||||||
content: String,
|
content: String,
|
||||||
role: String,
|
role: String,
|
||||||
resume_hint: String,
|
resume_hint: String,
|
||||||
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
subagent_task_id: Option<String>,
|
||||||
},
|
},
|
||||||
#[serde(rename = "error")]
|
#[serde(rename = "error")]
|
||||||
Error { code: String, message: String },
|
Error { code: String, message: String },
|
||||||
|
|||||||
@ -21,6 +21,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
role: message.role.clone(),
|
role: message.role.clone(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
|
subagent_task_id: None,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -40,6 +41,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
role: message.role.clone(),
|
role: message.role.clone(),
|
||||||
attachments: Vec::new(),
|
attachments: Vec::new(),
|
||||||
|
subagent_task_id: None,
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -63,6 +65,7 @@ pub(crate) fn ws_outbound_from_chat_message(message: &ChatMessage) -> Vec<WsOutb
|
|||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
role: message.role.clone(),
|
role: message.role.clone(),
|
||||||
resume_hint: TOOL_PENDING_RESUME_HINT.to_string(),
|
resume_hint: TOOL_PENDING_RESUME_HINT.to_string(),
|
||||||
|
subagent_task_id: None,
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
_ => Vec::new(),
|
_ => Vec::new(),
|
||||||
@ -88,6 +91,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
role: message.role.clone(),
|
role: message.role.clone(),
|
||||||
attachments,
|
attachments,
|
||||||
|
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
||||||
}]
|
}]
|
||||||
}
|
}
|
||||||
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
|
OutboundEventKind::ToolCall => vec![WsOutbound::ToolCall {
|
||||||
@ -126,6 +130,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
|
|||||||
content: message.content.clone(),
|
content: message.content.clone(),
|
||||||
role: message.role.clone(),
|
role: message.role.clone(),
|
||||||
resume_hint: TOOL_PENDING_RESUME_HINT.to_string(),
|
resume_hint: TOOL_PENDING_RESUME_HINT.to_string(),
|
||||||
|
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
|
||||||
}],
|
}],
|
||||||
OutboundEventKind::ErrorNotification => vec![WsOutbound::Error {
|
OutboundEventKind::ErrorNotification => vec![WsOutbound::Error {
|
||||||
code: "AGENT_ERROR".to_string(),
|
code: "AGENT_ERROR".to_string(),
|
||||||
|
|||||||
@ -7,7 +7,7 @@ use std::time::Duration;
|
|||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::agent::{AgentLoop, AgentRuntimeConfig, EmittedMessageHandler, SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
use crate::agent::{AgentLoop, AgentRuntimeConfig, EmittedMessageHandler, PersistingEmittedMessageHandler, SystemPrompt, SystemPromptContext, SystemPromptProvider};
|
||||||
use crate::bus::ChatMessage;
|
use crate::bus::ChatMessage;
|
||||||
use crate::bus::message::OutboundMessage;
|
use crate::bus::message::OutboundMessage;
|
||||||
use crate::bus::MessageBus;
|
use crate::bus::MessageBus;
|
||||||
@ -227,12 +227,16 @@ impl DefaultSubAgentRuntime {
|
|||||||
metadata.insert("subagent_task_id".to_string(), session.id.clone());
|
metadata.insert("subagent_task_id".to_string(), session.id.clone());
|
||||||
metadata.insert("is_subagent_event".to_string(), "true".to_string());
|
metadata.insert("is_subagent_event".to_string(), "true".to_string());
|
||||||
|
|
||||||
let emitter = Arc::new(SubAgentEmitter {
|
let emitter = Arc::new(PersistingEmittedMessageHandler::new(
|
||||||
bus: bus.clone(),
|
SubAgentEmitter {
|
||||||
channel_name: session.parent_channel_name.clone(),
|
bus: bus.clone(),
|
||||||
chat_id: session.parent_chat_id.clone(),
|
channel_name: session.parent_channel_name.clone(),
|
||||||
metadata,
|
chat_id: session.parent_chat_id.clone(),
|
||||||
});
|
metadata,
|
||||||
|
},
|
||||||
|
self.conversation_repository.clone(),
|
||||||
|
session.session_id.clone(),
|
||||||
|
));
|
||||||
|
|
||||||
return agent.with_emitted_message_handler(emitter);
|
return agent.with_emitted_message_handler(emitter);
|
||||||
}
|
}
|
||||||
@ -274,11 +278,6 @@ impl DefaultSubAgentRuntime {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(process_result)) => {
|
Ok(Ok(process_result)) => {
|
||||||
// 保存子智能体产生的所有消息到数据库(批量单事务)
|
|
||||||
self.conversation_repository
|
|
||||||
.append_messages_batch(&session.session_id, None, &process_result.emitted_messages)
|
|
||||||
.map_err(TaskError::RepositoryError)?;
|
|
||||||
|
|
||||||
let final_message = process_result.final_response;
|
let final_message = process_result.final_response;
|
||||||
Ok(TaskToolResult {
|
Ok(TaskToolResult {
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
@ -324,11 +323,6 @@ impl DefaultSubAgentRuntime {
|
|||||||
|
|
||||||
match result {
|
match result {
|
||||||
Ok(Ok(process_result)) => {
|
Ok(Ok(process_result)) => {
|
||||||
// 保存子智能体产生的所有消息到数据库(批量单事务)
|
|
||||||
self.conversation_repository
|
|
||||||
.append_messages_batch(&session.session_id, None, &process_result.emitted_messages)
|
|
||||||
.map_err(TaskError::RepositoryError)?;
|
|
||||||
|
|
||||||
let final_message = process_result.final_response;
|
let final_message = process_result.final_response;
|
||||||
Ok(TaskToolResult {
|
Ok(TaskToolResult {
|
||||||
status: "success".to_string(),
|
status: "success".to_string(),
|
||||||
|
|||||||
@ -300,6 +300,19 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
<span>点击查看子智能体输出</span>
|
<span>点击查看子智能体输出</span>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
) : isTaskTool && message.subagentTaskId ? (
|
||||||
|
<div className="px-3 pb-1">
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务')
|
||||||
|
}}
|
||||||
|
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span>查看实时进度</span>
|
||||||
|
<span>→</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
) : hasResult ? (
|
) : hasResult ? (
|
||||||
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
<div className="px-3 pb-2 text-xs text-[#00f0ff]/50 flex items-center gap-1 select-none">
|
||||||
<span>点击查看工具结果</span>
|
<span>点击查看工具结果</span>
|
||||||
@ -378,6 +391,18 @@ export function MessageBubble({ message, onNavigateToSubAgent }: MessageBubblePr
|
|||||||
{!hasArgs && !hasResult && (
|
{!hasArgs && !hasResult && (
|
||||||
<div className="text-xs text-zinc-500">等待工具执行...</div>
|
<div className="text-xs text-zinc-500">等待工具执行...</div>
|
||||||
)}
|
)}
|
||||||
|
{isTaskTool && message.subagentTaskId && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
onNavigateToSubAgent?.(message.subagentTaskId!, taskDescription || '子智能体任务')
|
||||||
|
}}
|
||||||
|
className="text-xs text-[#00f0ff] hover:text-[#00f0ff]/80 hover:underline transition-colors flex items-center gap-1"
|
||||||
|
>
|
||||||
|
<span>查看实时进度</span>
|
||||||
|
<span>→</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -98,8 +98,9 @@ export function useChat(): UseChatReturn {
|
|||||||
|
|
||||||
// Extract subagent_task_id from a message if present
|
// Extract subagent_task_id from a message if present
|
||||||
const getSubagentTaskId = (message: WsOutbound): string | undefined => {
|
const getSubagentTaskId = (message: WsOutbound): string | undefined => {
|
||||||
if (message.type === 'tool_call' || message.type === 'tool_result') {
|
if (message.type === 'tool_call' || message.type === 'tool_result'
|
||||||
return (message as ToolCall | ToolResult).subagent_task_id
|
|| message.type === 'tool_pending' || message.type === 'assistant_response') {
|
||||||
|
return (message as ToolCall | ToolResult | ToolPending | AssistantResponse).subagent_task_id
|
||||||
}
|
}
|
||||||
return undefined
|
return undefined
|
||||||
}
|
}
|
||||||
@ -117,6 +118,7 @@ export function useChat(): UseChatReturn {
|
|||||||
timestamp: Date.now(),
|
timestamp: Date.now(),
|
||||||
type: 'message',
|
type: 'message',
|
||||||
attachments: msg.attachments,
|
attachments: msg.attachments,
|
||||||
|
subagentTaskId: msg.subagent_task_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'tool_call': {
|
case 'tool_call': {
|
||||||
@ -156,6 +158,7 @@ export function useChat(): UseChatReturn {
|
|||||||
type: 'tool_pending',
|
type: 'tool_pending',
|
||||||
toolName: msg.tool_name,
|
toolName: msg.tool_name,
|
||||||
toolCallId: msg.tool_call_id,
|
toolCallId: msg.tool_call_id,
|
||||||
|
subagentTaskId: msg.subagent_task_id,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
case 'error': {
|
case 'error': {
|
||||||
@ -215,8 +218,20 @@ export function useChat(): UseChatReturn {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// In main view, skip sub-agent messages (they belong to sub-agent view)
|
// In main view, skip sub-agent messages (they belong to sub-agent view).
|
||||||
if (getSubagentTaskId(message)) {
|
// But use the task_id to associate with the running task tool card.
|
||||||
|
const msgSubagentTaskId = getSubagentTaskId(message)
|
||||||
|
if (msgSubagentTaskId) {
|
||||||
|
setMessages((prev) => {
|
||||||
|
for (let i = prev.length - 1; i >= 0; i--) {
|
||||||
|
if (prev[i].type === 'tool_call' && prev[i].toolName === 'task' && !prev[i].subagentTaskId) {
|
||||||
|
const updated = [...prev]
|
||||||
|
updated[i] = { ...updated[i], subagentTaskId: msgSubagentTaskId }
|
||||||
|
return updated
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return prev
|
||||||
|
})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -41,6 +41,7 @@ export interface AssistantResponse {
|
|||||||
content: string
|
content: string
|
||||||
role: string
|
role: string
|
||||||
attachments?: Attachment[]
|
attachments?: Attachment[]
|
||||||
|
subagent_task_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ToolCall {
|
export interface ToolCall {
|
||||||
@ -72,6 +73,7 @@ export interface ToolPending {
|
|||||||
content: string
|
content: string
|
||||||
role: string
|
role: string
|
||||||
resume_hint: string
|
resume_hint: string
|
||||||
|
subagent_task_id?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WsError {
|
export interface WsError {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user