refactor: unify turn progress delivery
This commit is contained in:
parent
42fe650785
commit
355244a3d6
@ -185,6 +185,8 @@ Session ID 格式为:
|
|||||||
|
|
||||||
SessionManager 负责组装会话上下文:系统提示、Skills、召回的 Knowledge、压缩后的 Timeline、可选的 active plan 摘要和当前消息历史。`session::turn_input` 在 Session 锁外并行读取 Knowledge、active plan 并压缩历史,然后通过同一个 assembly 路径生成首次请求和 context-overflow 重试输入;重试不得复制系统提示或 runtime context 拼接逻辑。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态。
|
SessionManager 负责组装会话上下文:系统提示、Skills、召回的 Knowledge、压缩后的 Timeline、可选的 active plan 摘要和当前消息历史。`session::turn_input` 在 Session 锁外并行读取 Knowledge、active plan 并压缩历史,然后通过同一个 assembly 路径生成首次请求和 context-overflow 重试输入;重试不得复制系统提示或 runtime context 拼接逻辑。普通闲聊 session 没有 plan 摘要;计划状态由 `WorkManager` 从 SQLite 读取,因此不以自然语言摘要作为权威来源。`AgentLoop` 接收完整输入执行一次模型/工具循环,本身不拥有会话状态。
|
||||||
|
|
||||||
|
当前 Turn 的工具进度只从 `AgentLoop` 的结构化 `TurnEvent` 进入 `TurnController`,不能另建字符串 notification 通道重复投递。后台子 Agent 的 `TaskNotification` 表达跨 Turn 的任务完成,仍由独立的受监督消费者投递。自动标题属于非关键派生工作:Turn 持久化完成后由 `TaskSupervisor` 调度,Session worker 不等待模型生成;同一 Session 同时最多有一个标题任务,提交时仍校验标题保持默认值,避免覆盖用户改名。
|
||||||
|
|
||||||
每个 session 最多有一个 active plan,但 plan 内多个 item 可以分别绑定不同子 Agent 并行执行。主 Agent 通过 `todo` 创建和维护计划,通过 `delegate.plan_item_id` 委托;子 Agent 不获得 `todo` 或 `delegate`。领取 item 使用条件更新防止重复执行,迟到结果只有在 plan 仍 active 且 execution ID 匹配时才允许提交。
|
每个 session 最多有一个 active plan,但 plan 内多个 item 可以分别绑定不同子 Agent 并行执行。主 Agent 通过 `todo` 创建和维护计划,通过 `delegate.plan_item_id` 委托;子 Agent 不获得 `todo` 或 `delegate`。领取 item 使用条件更新防止重复执行,迟到结果只有在 plan 仍 active 且 execution ID 匹配时才允许提交。
|
||||||
|
|
||||||
## 6. 持久化
|
## 6. 持久化
|
||||||
@ -213,7 +215,7 @@ SessionManager 负责组装会话上下文:系统提示、Skills、召回的 K
|
|||||||
|
|
||||||
## 7. 后台任务与生命周期
|
## 7. 后台任务与生命周期
|
||||||
|
|
||||||
`TaskSupervisor` 是 Gateway 内部后台任务的统一所有者。inbound/control routers、inbound lanes、outbound dispatcher、scheduler、session workers、Turn delivery、outbound lanes、通知消费者和子 Agent 后台任务都应通过它注册。
|
`TaskSupervisor` 是 Gateway 内部后台任务的统一所有者。inbound/control routers、inbound lanes、outbound dispatcher、scheduler、session workers、Turn delivery、outbound lanes、后台任务通知消费者、自动标题和子 Agent 后台任务都应通过它注册。
|
||||||
|
|
||||||
两种注册方式:
|
两种注册方式:
|
||||||
|
|
||||||
|
|||||||
@ -305,7 +305,6 @@ pub struct AgentLoop {
|
|||||||
workspace_dir: PathBuf,
|
workspace_dir: PathBuf,
|
||||||
model_name: String,
|
model_name: String,
|
||||||
context_window: usize,
|
context_window: usize,
|
||||||
notify_tx: Option<tokio::sync::mpsc::UnboundedSender<String>>,
|
|
||||||
input_types: Vec<String>,
|
input_types: Vec<String>,
|
||||||
media_registry: MediaHandlerRegistry,
|
media_registry: MediaHandlerRegistry,
|
||||||
}
|
}
|
||||||
@ -356,7 +355,6 @@ impl AgentLoop {
|
|||||||
provider: Arc::from(provider),
|
provider: Arc::from(provider),
|
||||||
tools: Arc::new(ToolRegistry::new()),
|
tools: Arc::new(ToolRegistry::new()),
|
||||||
observer: None,
|
observer: None,
|
||||||
notify_tx: None,
|
|
||||||
context_window: 0,
|
context_window: 0,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
@ -382,7 +380,6 @@ impl AgentLoop {
|
|||||||
provider: Arc::from(provider),
|
provider: Arc::from(provider),
|
||||||
tools,
|
tools,
|
||||||
observer: None,
|
observer: None,
|
||||||
notify_tx: None,
|
|
||||||
context_window: 0,
|
context_window: 0,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
@ -404,7 +401,6 @@ impl AgentLoop {
|
|||||||
provider,
|
provider,
|
||||||
tools: Arc::new(ToolRegistry::new()),
|
tools: Arc::new(ToolRegistry::new()),
|
||||||
observer: None,
|
observer: None,
|
||||||
notify_tx: None,
|
|
||||||
context_window: 0,
|
context_window: 0,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
@ -427,7 +423,6 @@ impl AgentLoop {
|
|||||||
provider,
|
provider,
|
||||||
tools,
|
tools,
|
||||||
observer: None,
|
observer: None,
|
||||||
notify_tx: None,
|
|
||||||
context_window: 0,
|
context_window: 0,
|
||||||
max_iterations,
|
max_iterations,
|
||||||
workspace_dir,
|
workspace_dir,
|
||||||
@ -455,11 +450,6 @@ impl AgentLoop {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn with_notify(mut self, tx: tokio::sync::mpsc::UnboundedSender<String>) -> Self {
|
|
||||||
self.notify_tx = Some(tx);
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Preemptive trim: truncate old tool results in-place when history is
|
/// Preemptive trim: truncate old tool results in-place when history is
|
||||||
/// approaching the context window limit. Old results (outside of `keep_recent`
|
/// approaching the context window limit. Old results (outside of `keep_recent`
|
||||||
/// zone) are replaced with a short placeholder; recent results are truncated
|
/// zone) are replaced with a short placeholder; recent results are truncated
|
||||||
@ -718,7 +708,8 @@ impl AgentLoop {
|
|||||||
.map_err(|error| AgentError::Other(format!("turn event rejected: {error}")))?;
|
.map_err(|error| AgentError::Other(format!("turn event rejected: {error}")))?;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Execute tool calls — log and notify immediately
|
// Execute tool calls. User-visible progress is emitted through the
|
||||||
|
// structured TurnEvent stream, not a second notification channel.
|
||||||
{
|
{
|
||||||
let tools_info: Vec<String> = response
|
let tools_info: Vec<String> = response
|
||||||
.tool_calls
|
.tool_calls
|
||||||
@ -726,9 +717,6 @@ impl AgentLoop {
|
|||||||
.map(|tc| {
|
.map(|tc| {
|
||||||
let args = serde_json::to_string(&tc.arguments).unwrap_or_default();
|
let args = serde_json::to_string(&tc.arguments).unwrap_or_default();
|
||||||
let s = format!("{}:{}", tc.name, args);
|
let s = format!("{}:{}", tc.name, args);
|
||||||
if let Some(ref tx) = self.notify_tx {
|
|
||||||
let _ = tx.send(format!("调用工具 {}", s));
|
|
||||||
}
|
|
||||||
s
|
s
|
||||||
})
|
})
|
||||||
.collect();
|
.collect();
|
||||||
|
|||||||
@ -376,6 +376,8 @@ pub struct Session {
|
|||||||
active_turn_emitter: Option<ActiveTurnEmitter>,
|
active_turn_emitter: Option<ActiveTurnEmitter>,
|
||||||
/// Monotonic counter to detect stale workers
|
/// Monotonic counter to detect stale workers
|
||||||
worker_generation: u64,
|
worker_generation: u64,
|
||||||
|
/// Prevents duplicate background title requests while the title is still default.
|
||||||
|
title_generation_in_flight: bool,
|
||||||
/// Monotonic counter for in-memory session mutations.
|
/// Monotonic counter for in-memory session mutations.
|
||||||
///
|
///
|
||||||
/// Slow work such as memory recall, compression, and title generation runs
|
/// Slow work such as memory recall, compression, and title generation runs
|
||||||
@ -468,6 +470,7 @@ impl Session {
|
|||||||
current_cancel: None,
|
current_cancel: None,
|
||||||
active_turn_emitter: None,
|
active_turn_emitter: None,
|
||||||
worker_generation: 0,
|
worker_generation: 0,
|
||||||
|
title_generation_in_flight: false,
|
||||||
state_version: 0,
|
state_version: 0,
|
||||||
persistence_lock: Arc::new(Mutex::new(())),
|
persistence_lock: Arc::new(Mutex::new(())),
|
||||||
})
|
})
|
||||||
@ -657,6 +660,7 @@ impl Session {
|
|||||||
current_cancel: None,
|
current_cancel: None,
|
||||||
active_turn_emitter: None,
|
active_turn_emitter: None,
|
||||||
worker_generation: 0,
|
worker_generation: 0,
|
||||||
|
title_generation_in_flight: false,
|
||||||
state_version: 0,
|
state_version: 0,
|
||||||
persistence_lock: Arc::new(Mutex::new(())),
|
persistence_lock: Arc::new(Mutex::new(())),
|
||||||
})
|
})
|
||||||
@ -916,14 +920,6 @@ impl Session {
|
|||||||
.with_context_window(self.provider_config.token_limit))
|
.with_context_window(self.provider_config.token_limit))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 创建一个附通知通道的 AgentLoop 实例
|
|
||||||
pub fn create_agent_with_notify(
|
|
||||||
&self,
|
|
||||||
notify_tx: tokio::sync::mpsc::UnboundedSender<String>,
|
|
||||||
) -> Result<AgentLoop, AgentError> {
|
|
||||||
Ok(self.create_agent()?.with_notify(notify_tx))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 构建系统提示词(包含 AgentLoop 的基础提示词 + skills + memory)
|
/// 构建系统提示词(包含 AgentLoop 的基础提示词 + skills + memory)
|
||||||
pub fn build_system_prompt(&self, skills_prompt: &str) -> String {
|
pub fn build_system_prompt(&self, skills_prompt: &str) -> String {
|
||||||
let base_prompt = build_system_prompt(
|
let base_prompt = build_system_prompt(
|
||||||
@ -2442,17 +2438,13 @@ impl SessionManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn maybe_generate_title_outside_lock(session: Arc<Mutex<Session>>) -> Result<(), AgentError> {
|
async fn generate_title(
|
||||||
|
session: Arc<Mutex<Session>>,
|
||||||
|
provider: Arc<dyn LLMProvider>,
|
||||||
|
prompt: String,
|
||||||
|
) -> Result<(), AgentError> {
|
||||||
use crate::providers::{ChatCompletionRequest, ChatCompletionResponse, Message};
|
use crate::providers::{ChatCompletionRequest, ChatCompletionResponse, Message};
|
||||||
|
|
||||||
let (provider, prompt) = {
|
|
||||||
let guard = session.lock().await;
|
|
||||||
let Some(prompt) = guard.title_prompt_snapshot() else {
|
|
||||||
return Ok(());
|
|
||||||
};
|
|
||||||
(guard.provider.clone(), prompt)
|
|
||||||
};
|
|
||||||
|
|
||||||
let request = ChatCompletionRequest {
|
let request = ChatCompletionRequest {
|
||||||
messages: vec![Message::user(prompt)],
|
messages: vec![Message::user(prompt)],
|
||||||
temperature: Some(0.3),
|
temperature: Some(0.3),
|
||||||
@ -2487,6 +2479,39 @@ async fn maybe_generate_title_outside_lock(session: Arc<Mutex<Session>>) -> Resu
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn schedule_title_generation(
|
||||||
|
session: Arc<Mutex<Session>>,
|
||||||
|
supervisor: crate::task_supervisor::TaskSupervisor,
|
||||||
|
session_id: &str,
|
||||||
|
) {
|
||||||
|
let title_job = {
|
||||||
|
let mut guard = session.lock().await;
|
||||||
|
if guard.title_generation_in_flight {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
guard.title_prompt_snapshot().map(|prompt| {
|
||||||
|
guard.title_generation_in_flight = true;
|
||||||
|
(guard.provider.clone(), prompt)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let Some((provider, prompt)) = title_job else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let title_session = session.clone();
|
||||||
|
let task_session = session.clone();
|
||||||
|
let spawned = supervisor.spawn(format!("session-title:{session_id}"), async move {
|
||||||
|
if let Err(error) = generate_title(title_session, provider, prompt).await {
|
||||||
|
tracing::warn!(error = %error, "Failed to generate session title");
|
||||||
|
}
|
||||||
|
task_session.lock().await.title_generation_in_flight = false;
|
||||||
|
});
|
||||||
|
if !spawned {
|
||||||
|
session.lock().await.title_generation_in_flight = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn spawn_agent_worker(
|
fn spawn_agent_worker(
|
||||||
mut task_rx: mpsc::Receiver<AgentTask>,
|
mut task_rx: mpsc::Receiver<AgentTask>,
|
||||||
session: Arc<Mutex<Session>>,
|
session: Arc<Mutex<Session>>,
|
||||||
@ -2510,40 +2535,6 @@ fn spawn_agent_worker(
|
|||||||
let task_chan = task.channel.clone();
|
let task_chan = task.channel.clone();
|
||||||
let task_cid = task.chat_id.clone();
|
let task_cid = task.chat_id.clone();
|
||||||
let task_metadata = task.forwarded_metadata.clone();
|
let task_metadata = task.forwarded_metadata.clone();
|
||||||
let notification_session_id = unified_str.clone();
|
|
||||||
|
|
||||||
let (notify_tx, mut notify_rx) = mpsc::unbounded_channel();
|
|
||||||
|
|
||||||
// Spawn notification publisher
|
|
||||||
{
|
|
||||||
let bus = bus.clone();
|
|
||||||
let ch = task_chan.clone();
|
|
||||||
let cid = task_cid.clone();
|
|
||||||
worker_supervisor.spawn(
|
|
||||||
format!("session-notifications:{ch}:{cid}"),
|
|
||||||
async move {
|
|
||||||
while let Some(notif) = notify_rx.recv().await {
|
|
||||||
let mut metadata = HashMap::new();
|
|
||||||
metadata.insert("_type".to_string(), "notification".to_string());
|
|
||||||
metadata.insert(
|
|
||||||
"_session_id".to_string(),
|
|
||||||
notification_session_id.clone(),
|
|
||||||
);
|
|
||||||
let outbound = OutboundMessage {
|
|
||||||
channel: ch.clone(),
|
|
||||||
chat_id: cid.clone(),
|
|
||||||
content: notif,
|
|
||||||
reply_to: None,
|
|
||||||
media: vec![],
|
|
||||||
metadata,
|
|
||||||
delivery: None,
|
|
||||||
};
|
|
||||||
let _ = bus.publish_outbound(outbound).await;
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Phase 1: capture a stable session snapshot under lock.
|
// Phase 1: capture a stable session snapshot under lock.
|
||||||
// Memory recall and compression happen outside this block so
|
// Memory recall and compression happen outside this block so
|
||||||
// /stop and other commands are not blocked behind slow I/O or
|
// /stop and other commands are not blocked behind slow I/O or
|
||||||
@ -2589,7 +2580,7 @@ fn spawn_agent_worker(
|
|||||||
|
|
||||||
let history_raw = guard.get_history().to_vec();
|
let history_raw = guard.get_history().to_vec();
|
||||||
|
|
||||||
let agent = match guard.create_agent_with_notify(notify_tx) {
|
let agent = match guard.create_agent() {
|
||||||
Ok(a) => a,
|
Ok(a) => a,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::error!(error = %e, "Failed to create agent");
|
tracing::error!(error = %e, "Failed to create agent");
|
||||||
@ -2721,6 +2712,7 @@ fn spawn_agent_worker(
|
|||||||
let cid2 = task_cid.clone();
|
let cid2 = task_cid.clone();
|
||||||
let unified_str2 = unified_str.clone();
|
let unified_str2 = unified_str.clone();
|
||||||
let task_metadata2 = task_metadata.clone();
|
let task_metadata2 = task_metadata.clone();
|
||||||
|
let title_supervisor = worker_supervisor.clone();
|
||||||
let turn_lifecycle = &turn_controller;
|
let turn_lifecycle = &turn_controller;
|
||||||
let process_future = async move {
|
let process_future = async move {
|
||||||
let response_session_id = unified_str2.clone();
|
let response_session_id = unified_str2.clone();
|
||||||
@ -2929,9 +2921,12 @@ fn spawn_agent_worker(
|
|||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Err(e) = maybe_generate_title_outside_lock(session2.clone()).await {
|
schedule_title_generation(
|
||||||
tracing::warn!("failed to generate title: {}", e);
|
session2.clone(),
|
||||||
}
|
title_supervisor,
|
||||||
|
&response_session_id,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
if !live_delivery_started {
|
if !live_delivery_started {
|
||||||
let outbound = OutboundMessage {
|
let outbound = OutboundMessage {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user