Files
agcore/examples/bridge_keys_demo.rs
T
徐涛 46de111965 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。
2026-07-15 11:15:58 +08:00

198 lines
7.2 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! bridge_keys_demo —— bridge_keys 过滤 + 子↔子共享 namespace 示例。
//!
//! 演示:
//! 1. 父 session 设置 SessionMemorykey: "project_goal", "constraints", "noise"
//! 2. dispatch + bridge_keys = ["project_goal", "constraints"] → 只继承这两个
//! 3. 验证子 session 读到的 session_memory 与过滤一致
//! 4. 演示子↔子共享 namespacedispatch 时设 `shared_namespace`
//! 子 A 写入 `shared:{parent_id}:fact_x`,子 B 通过约定 key 读取
//!
//! 运行:`cargo run --example bridge_keys_demo`
use std::sync::Arc;
use agcore::agent::{Agent, AgentBuilder};
use agcore::engine::{DispatchConfig, SessionManager};
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 WorkerAgent;
impl Agent for WorkerAgent {
fn name(&self) -> &str {
"worker"
}
fn system_prompt(&self) -> Option<&str> {
Some("You are a worker.")
}
}
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(),
}
}
#[tokio::main]
async fn main() {
println!("=== Bridge Keys Demo ===\n");
let store: Arc<dyn agcore::memory::store::MemoryStore> = Arc::new(InMemoryStore::new());
// 3 个 dispatch 调用需要 3 个 mock response
let provider = Arc::new(MockProvider::new(vec![
assistant_text("Worker 1: done"),
assistant_text("Worker 2: done"),
assistant_text("Worker 3: done"),
]));
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 worker: Arc<dyn Agent> = Arc::new(WorkerAgent);
let sm = Arc::new(SessionManager::new(store));
let parent_id = sm
.create(worker.clone(), bundle.clone())
.await
.expect("create");
println!("[1] parent session created: {parent_id}");
// 父 session_memory 写入 3 个 key
{
let session = sm.get(&parent_id).await.unwrap();
let mut guard = session.lock().await;
guard
.set_session_data("project_goal", "Build a fast compiler")
.await
.unwrap();
guard
.set_session_data("constraints", "Rust, no unsafe")
.await
.unwrap();
guard
.set_session_data("noise", "should NOT be inherited")
.await
.unwrap();
}
println!("[2] parent set 3 keys: project_goal, constraints, noise");
// dispatch + bridge_keys 过滤
let config = DispatchConfig {
bridge_keys: Some(vec!["project_goal".to_string(), "constraints".to_string()]),
..Default::default()
};
let result = sm
.dispatch(&parent_id, worker.clone(), "do work", config)
.await
.expect("dispatch");
println!("[3] dispatched sub-agent (child_id={})\n", &result.child_id[..20]);
// 验证过滤效果
let child_session = sm.get(&result.child_id).await.unwrap();
let child_guard = child_session.lock().await;
let inherited_goal = child_guard.session_memory().get("project_goal").await.unwrap();
let inherited_constraint = child_guard.session_memory().get("constraints").await.unwrap();
let filtered_noise = child_guard.session_memory().get("noise").await.unwrap();
drop(child_guard);
println!("[verify] inherited keys in child session:");
println!(" - project_goal: {:?}", inherited_goal);
println!(" - constraints: {:?}", inherited_constraint);
println!(" - noise: {:?} (should be None)", filtered_noise);
assert_eq!(inherited_goal, Some("Build a fast compiler".to_string()));
assert_eq!(inherited_constraint, Some("Rust, no unsafe".to_string()));
assert_eq!(filtered_noise, None, "noise should be filtered out");
println!("\n✓ bridge_keys filtering works correctly");
// =============== 第二部分:子↔子共享 namespaceconvention===============
println!("\n=== Part 2: Child↔Child Shared Namespace (convention) ===\n");
// 关键点:`SessionMemory::get`/`set` 通过 session 自身 namespace 隔离
// (每个 session 一个独立 namespace),所以"子↔子共享"不能直接通过 SessionMemory。
// 真正的子↔子共享需要直接操作底层 MemoryStore,或由上层应用维护一个
// 跨 session 的"共享通道"(例如独立的 namespace + 所有子 session 知道 key 前缀)。
//
// 本 demo 演示通过 DispatchConfig.shared_namespaceconvention-based):
// - `shared_namespace: Some(prefix)` 作为约定标记,告知子 agent
// "你的数据共享 namespace 是 shared:{prefix}:*"
// - 子 agent 自行通过 `sm.store()` 直接操作 MemoryStore(绕过 SessionMemory 的 namespace 隔离)
//
// 演示 2 个子 agent 通过约定 namespace prefix 共享数据。
let shared_ns_config = DispatchConfig {
bridge_keys: Some(vec![]),
shared_namespace: Some("parent-123".to_string()),
..Default::default()
};
// dispatch 第一个子 agent
let _researcher_result = sm
.dispatch(
&parent_id,
worker.clone(),
"research task",
shared_ns_config.clone(),
)
.await
.expect("dispatch researcher");
// 子 A 通过 `sm.store()` 直接写入共享 namespace key
// (约定 prefix: "shared:parent-123:")
let shared_key = "shared:parent-123:fact_architecture";
sm.store()
.save(agcore::memory::types::MemoryItem {
id: shared_key.to_string(),
content: "Microservices with event sourcing".to_string(),
metadata: serde_json::json!({}),
created_at: time::OffsetDateTime::now_utc(),
})
.await
.expect("save shared fact");
println!("[4] researcher wrote {shared_key}");
// dispatch 第二个子 agent
let _writer_result = sm
.dispatch(
&parent_id,
worker.clone(),
"writing task",
shared_ns_config,
)
.await
.expect("dispatch writer");
// 子 B 通过 `sm.store()` 直接读取共享 namespace key
let read_item = sm.store().get(shared_key).await.expect("get");
let read_fact = read_item.map(|i| i.content);
println!("[5] writer reads {shared_key} = {read_fact:?}");
assert_eq!(
read_fact,
Some("Microservices with event sourcing".to_string()),
"writer should read researcher's shared fact"
);
println!("\n✓ child↔child shared namespace works correctly (via MemoryStore convention)");
println!("\n=== All Bridge Keys Demo checks passed ===");
}