feat: 新增专家提示词核心模块,支持文件发现、CRUD与运行时选择

- 新增 src/experts/mod.rs 模块,镜像 skills/subagents 模式

- 支持 user/project 双 scope 从 EXPERT.md 文件发现专家

- ExpertRuntime 维护 session→expert 映射与禁用列表,持久化到 expert-state.json

- ExpertPromptProvider 实现系统提示词注入,未选中时返回 None

- 包含 16 个单元测试覆盖 frontmatter 解析、discover 优先级、CRUD、select/clear、disable 场景
This commit is contained in:
oudecheng 2026-07-06 18:07:10 +08:00
parent 8d30ccd020
commit c2d9bf8b5e
6 changed files with 1503 additions and 0 deletions

View File

@ -67,6 +67,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
code: error.code, code: error.code,
message: error.message, message: error.message,
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: None,
}); });
return outbounds; return outbounds;
} }
@ -197,6 +198,7 @@ impl OutputAdapter for WebSocketOutputAdapter {
code: "RESPONSE_ERROR".to_string(), code: "RESPONSE_ERROR".to_string(),
message: msg.content.clone(), message: msg.content.clone(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: None,
}, },
_ => WsOutbound::AssistantResponse { _ => WsOutbound::AssistantResponse {
id: response.request_id.to_string(), id: response.request_id.to_string(),

1484
src/experts/mod.rs Normal file

File diff suppressed because it is too large Load Diff

View File

@ -267,6 +267,7 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
code:"SESSION_ERROR".to_string(), code:"SESSION_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
subagent_task_id: None,
}) })
.await; .await;
} }
@ -278,6 +279,7 @@ async fn handle_socket(ws: WebSocket, state: Arc<GatewayState>) {
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
code:"PARSE_ERROR".to_string(), code:"PARSE_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
subagent_task_id: None,
}) })
.await; .await;
} }
@ -370,6 +372,7 @@ async fn handle_inbound(
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
code: "INVALID_COMMAND".to_string(), code: "INVALID_COMMAND".to_string(),
message: "Invalid command payload".to_string(), message: "Invalid command payload".to_string(),
subagent_task_id: None,
}) })
.await; .await;
return Ok(()); return Ok(());
@ -380,6 +383,7 @@ async fn handle_inbound(
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
code: "PARSE_ERROR".to_string(), code: "PARSE_ERROR".to_string(),
message: e.to_string(), message: e.to_string(),
subagent_task_id: None,
}) })
.await; .await;
return Ok(()); return Ok(());
@ -796,6 +800,12 @@ fn set_subagent_task_id(outbound: &mut WsOutbound, task_id: &str) {
} }
| WsOutbound::ToolPending { | WsOutbound::ToolPending {
subagent_task_id, .. subagent_task_id, ..
}
| WsOutbound::StreamDelta {
subagent_task_id, ..
}
| WsOutbound::StreamEnd {
subagent_task_id, ..
} => { } => {
*subagent_task_id = Some(task_id.to_string()); *subagent_task_id = Some(task_id.to_string());
} }

View File

@ -7,6 +7,7 @@ pub mod client;
pub mod command; pub mod command;
pub mod config; pub mod config;
pub mod domain; pub mod domain;
pub mod experts;
pub mod gateway; pub mod gateway;
pub mod logging; pub mod logging;
pub mod mcp; pub mod mcp;

View File

@ -209,6 +209,8 @@ pub enum WsOutbound {
message: String, message: String,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>, timestamp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
subagent_task_id: Option<String>,
}, },
#[serde(rename = "task_started")] #[serde(rename = "task_started")]
TaskStarted { TaskStarted {
@ -301,6 +303,8 @@ pub enum WsOutbound {
topic_id: Option<String>, topic_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
timestamp: Option<i64>, timestamp: Option<i64>,
#[serde(default, skip_serializing_if = "Option::is_none")]
subagent_task_id: Option<String>,
}, },
#[serde(rename = "todo_list")] #[serde(rename = "todo_list")]
TodoList { TodoList {

View File

@ -172,6 +172,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
code: "AGENT_ERROR".to_string(), code: "AGENT_ERROR".to_string(),
message: message.content.clone(), message: message.content.clone(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
}], }],
OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted { OutboundEventKind::TaskStarted => vec![WsOutbound::TaskStarted {
task_id: message.metadata.get("task_id").cloned().unwrap_or_default(), task_id: message.metadata.get("task_id").cloned().unwrap_or_default(),
@ -197,6 +198,7 @@ pub(crate) fn ws_outbound_from_outbound_message(message: &OutboundMessage) -> Ve
OutboundEventKind::ExecutionCompleted => vec![WsOutbound::ExecutionCompleted { OutboundEventKind::ExecutionCompleted => vec![WsOutbound::ExecutionCompleted {
topic_id: message.metadata.get("topic_id").cloned(), topic_id: message.metadata.get("topic_id").cloned(),
timestamp: Some(crate::protocol::now_timestamp()), timestamp: Some(crate::protocol::now_timestamp()),
subagent_task_id: message.metadata.get("subagent_task_id").cloned(),
}], }],
} }
} }