feat(engine): 实现 Agent 角色热切换与子代理调度

新增 SessionManager::switch_agent / dispatch / dispatch_all / dispatch_stream
四个核心方法,补齐多 Agent 基础系统原语。

交付物:
- switch_agent 运行时替换 Arc<dyn Agent>,保留上下文并更新 SessionMeta
- dispatch 单任务派发:create_child → inherit_memory → submit_turn
- dispatch_all 并行派发:Semaphore 并发控制 + 部分成功语义
- dispatch_stream 流式派发:unbounded_channel + spawn task 消息重建 + finalize_turn
- DispatchConfig / SubTaskResult / SubTaskStreamEvent 公开类型
- 4 个端到端示例(agent_switch_demo / sub_agent_dispatch_demo /
  bridge_keys_demo / dispatch_stream_demo)

辅助变更:
- EngineError 新增 DispatchFailed 变体
- CostTracker 实现 From<Usage> 转换
- save_session_meta / load_session_meta 改 pub(crate) 供 switch.rs 使用

测试: +17 个内联测试(4 switch + 5 dispatch + 4 dispatch_all +
4 dispatch_stream),全量 391 passed / 0 failed,
clippy 0 警告,doc 0 warning。
This commit is contained in:
徐涛
2026-07-15 11:15:58 +08:00
parent cb922b03de
commit 46de111965
11 changed files with 2586 additions and 3 deletions
+141
View File
@@ -0,0 +1,141 @@
//! sub_agent_dispatch_demo —— SubAgent 并行派发示例。
//!
//! 演示:
//! 1. 创建父 session"主编" agent
//! 2. 并行 dispatch_all 3 个子 agent(研究员 / 写手 / 审校)
//! 3. 收集子任务结果
//! 4. 验证树形结构:children(parent_id) 应返回 3 个子 ID
//!
//! 运行:`cargo run --example sub_agent_dispatch_demo`
use std::sync::Arc;
use agcore::agent::{Agent, AgentBuilder};
use agcore::engine::{SessionManager, SubTaskResult};
use agcore::llm::hooks::HookExecutor;
use agcore::llm::mock::MockProvider;
use agcore::llm::types::Usage;
use agcore::llm::types::message::{ContentBlock, Message};
use agcore::llm::types::response_v2::{MessageResponse, StopReason};
use agcore::memory::store::InMemoryStore;
use agcore::tools::ToolRegistry;
struct EditorAgent;
struct ResearcherAgent;
struct WriterAgent;
struct ReviewerAgent;
impl Agent for EditorAgent {
fn name(&self) -> &str {
"editor"
}
fn system_prompt(&self) -> Option<&str> {
Some("You are an editor coordinating a team.")
}
}
impl Agent for ResearcherAgent {
fn name(&self) -> &str {
"researcher"
}
fn system_prompt(&self) -> Option<&str> {
Some("You are a researcher. Provide 3 key findings.")
}
}
impl Agent for WriterAgent {
fn name(&self) -> &str {
"writer"
}
fn system_prompt(&self) -> Option<&str> {
Some("You are a writer. Draft a section.")
}
}
impl Agent for ReviewerAgent {
fn name(&self) -> &str {
"reviewer"
}
fn system_prompt(&self) -> Option<&str> {
Some("You are a reviewer. Check for accuracy.")
}
}
fn assistant_text(text: &str) -> MessageResponse {
MessageResponse {
id: String::new(),
model: String::new(),
message: Message::Assistant {
content: vec![ContentBlock::Text { text: text.into() }],
},
usage: Usage::from_input_output(8, 4),
stop_reason: StopReason::Stop,
extra: Default::default(),
}
}
fn print_result(name: &str, r: &Result<SubTaskResult, agcore::engine::EngineError>) {
match r {
Ok(res) => println!(
"{name} (child_id={}): {} tokens",
&res.child_id[..20.min(res.child_id.len())],
res.usage.total().total_tokens,
),
Err(e) => println!("{name}: {e}"),
}
}
#[tokio::main]
async fn main() {
println!("=== SubAgent Dispatch Demo ===\n");
let store: Arc<dyn agcore::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
let provider = Arc::new(MockProvider::new(vec![
assistant_text("Researcher: finding 1, 2, 3"),
assistant_text("Writer: section drafted"),
assistant_text("Reviewer: looks good"),
]));
let bundle = Arc::new(
AgentBuilder::new()
.provider(provider)
.tool_registry(Arc::new(ToolRegistry::new()))
.hook_executor(Arc::new(HookExecutor::new()))
.session_memory_backend(store.clone())
.build()
.expect("RuntimeBundle"),
);
let editor: Arc<dyn Agent> = Arc::new(EditorAgent);
let researcher: Arc<dyn Agent> = Arc::new(ResearcherAgent);
let writer: Arc<dyn Agent> = Arc::new(WriterAgent);
let reviewer: Arc<dyn Agent> = Arc::new(ReviewerAgent);
let sm = Arc::new(SessionManager::new(store));
let parent_id = sm.create(editor, bundle.clone()).await.expect("create");
println!("[1] parent session created: {parent_id}");
// dispatch_all 3 个子 agent
println!("[2] dispatching 3 sub-agents in parallel...\n");
let results = sm
.dispatch_all(
&parent_id,
vec![
(researcher, "Research topic X".to_string()),
(writer, "Draft intro section".to_string()),
(reviewer, "Review draft".to_string()),
],
agcore::engine::DispatchConfig::default(),
)
.await;
print_result("researcher", &results[0]);
print_result("writer", &results[1]);
print_result("reviewer", &results[2]);
let success_count = results.iter().filter(|r| r.is_ok()).count();
assert_eq!(success_count, 3, "all 3 should succeed");
// 验证树形
let children = sm.children(&parent_id).await.expect("children");
println!("\n[3] children(parent) = {} session(s)", children.len());
assert_eq!(children.len(), 3);
println!("\n✓ dispatch_all completed: 3/3 sub-agents succeeded");
}