f7a720204a
- 移除 GovAI, nomifun-tauri, 算力盒子 的 submodule 引用 - 添加所有子项目的完整源代码 - 保留原始 .git 为 .git.bak 备份
189 lines
8.2 KiB
Rust
189 lines
8.2 KiB
Rust
use crate::manager::acp::AcpAgentManager;
|
|
|
|
use crate::manager::acp::mode_normalize::normalize_requested_mode;
|
|
use crate::protocol::error::AcpError;
|
|
use crate::shared_kernel::{ConfigKey, ConfigValue, ModeId, ModelId};
|
|
use agent_client_protocol::schema::{
|
|
SessionId, SetSessionConfigOptionRequest, SetSessionModeRequest, SetSessionModelRequest,
|
|
};
|
|
use nomifun_common::AppError;
|
|
use tracing::{error, info, warn};
|
|
|
|
/// Actions the session driver must execute to align CLI state with user intent.
|
|
///
|
|
/// Produced by `AcpSession::plan_reconcile` — a pure function that compares
|
|
/// desired vs observed and returns a list of idempotent, order-independent ops.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum ReconcileAction {
|
|
SetMode { mode: ModeId },
|
|
SetModel { model: ModelId },
|
|
SetConfigOption { key: ConfigKey, value: ConfigValue },
|
|
}
|
|
|
|
impl AcpAgentManager {
|
|
/// Execute reconcile actions produced by `AcpSession::plan_reconcile`.
|
|
///
|
|
/// Compares the aggregate's desired state against what the CLI has
|
|
/// reported as current, then issues the minimal set of SDK calls
|
|
/// (set_mode, set_model, set_config_option) to bring the CLI into
|
|
/// alignment.
|
|
///
|
|
/// Failure handling:
|
|
/// - `SessionNotFound`: returned as `AppError::NotFound` so callers
|
|
/// (e.g. `open_session_resume`) can drop the stale sid and rebuild
|
|
/// the session. ELECTRON-1HQ regressed because we silently swallowed
|
|
/// this case during warmup, leaving downstream `session/prompt` to
|
|
/// surface the same error to the user every turn.
|
|
/// - Any other error: logged and skipped (best-effort), so a failed
|
|
/// `set_config_option` doesn't block a successful `set_mode`.
|
|
pub(super) async fn reconcile_session(&self, session_id: &str) -> Result<(), AppError> {
|
|
use crate::manager::acp::ReconcileAction;
|
|
|
|
let (invalid_model, actions) = {
|
|
let mut session = self.session.write().await;
|
|
let invalid_model = session.clear_invalid_desired_model();
|
|
let actions = session.plan_reconcile();
|
|
(invalid_model, actions)
|
|
};
|
|
if let Some(model) = invalid_model {
|
|
warn!(
|
|
conversation_id = %self.params.conversation_id,
|
|
model_id = %model,
|
|
"reconcile_session: dropped unavailable desired model"
|
|
);
|
|
}
|
|
for action in actions {
|
|
match action {
|
|
ReconcileAction::SetMode { mode } => {
|
|
let normalized = normalize_requested_mode(&self.params.metadata, mode.as_str());
|
|
if normalized.is_empty() {
|
|
continue;
|
|
}
|
|
if let Err(e) = self
|
|
.protocol
|
|
.set_mode(SetSessionModeRequest::new(
|
|
SessionId::new(session_id),
|
|
normalized.clone(),
|
|
))
|
|
.await
|
|
{
|
|
if matches!(e, AcpError::SessionNotFound { .. }) {
|
|
warn!(
|
|
conversation_id = %self.params.conversation_id,
|
|
mode_id = %normalized,
|
|
error = %e,
|
|
"reconcile_session: set_mode hit SessionNotFound; aborting reconcile"
|
|
);
|
|
return Err(AppError::from(e));
|
|
}
|
|
error!(
|
|
conversation_id = %self.params.conversation_id,
|
|
mode_id = %normalized,
|
|
error = %e,
|
|
"reconcile_session: set_mode failed"
|
|
);
|
|
continue;
|
|
}
|
|
// SDK does not push a notification after a successful
|
|
// set_mode — sync observed/advertised ourselves so the
|
|
// next plan_reconcile is a no-op.
|
|
let mut session = self.session.write().await;
|
|
session.apply_observed_mode(ModeId::new(normalized));
|
|
self.commit_session_changes(&mut session).await;
|
|
}
|
|
|
|
ReconcileAction::SetModel { model } => {
|
|
if let Err(e) = self
|
|
.protocol
|
|
.set_model(SetSessionModelRequest::new(
|
|
SessionId::new(session_id),
|
|
model.as_str().to_owned(),
|
|
))
|
|
.await
|
|
{
|
|
if matches!(e, AcpError::SessionNotFound { .. }) {
|
|
warn!(
|
|
conversation_id = %self.params.conversation_id,
|
|
model_id = %model,
|
|
error = %e,
|
|
"reconcile_session: set_model hit SessionNotFound; aborting reconcile"
|
|
);
|
|
return Err(AppError::from(e));
|
|
}
|
|
error!(
|
|
conversation_id = %self.params.conversation_id,
|
|
model_id = %model,
|
|
error = %e,
|
|
"reconcile_session: set_model failed"
|
|
);
|
|
continue;
|
|
}
|
|
// SDK does not push a CurrentModelUpdate notification —
|
|
// sync observed/advertised ourselves.
|
|
let mut session = self.session.write().await;
|
|
let model_for_notice = model.clone();
|
|
session.apply_observed_model(model);
|
|
if self.params.metadata.behavior_policy.self_identity_sticky {
|
|
session.set_pending_model_notice(model_for_notice);
|
|
}
|
|
self.commit_session_changes(&mut session).await;
|
|
}
|
|
|
|
ReconcileAction::SetConfigOption { key, value } => {
|
|
if let Err(err) = self
|
|
.protocol
|
|
.set_config_option(SetSessionConfigOptionRequest::new(
|
|
SessionId::new(session_id),
|
|
key.as_str().to_owned(),
|
|
value.as_str().to_owned(),
|
|
))
|
|
.await
|
|
{
|
|
if matches!(err, AcpError::SessionNotFound { .. }) {
|
|
warn!(
|
|
conversation_id = %self.params.conversation_id,
|
|
config_id = %key,
|
|
desired = %value,
|
|
error = %err,
|
|
"reconcile_session: set_config_option hit SessionNotFound; aborting reconcile"
|
|
);
|
|
return Err(AppError::from(err));
|
|
}
|
|
info!(
|
|
conversation_id = %self.params.conversation_id,
|
|
config_id = %key,
|
|
desired = %value,
|
|
error = %err,
|
|
"reconcile_session: set_config_option failed; skipping"
|
|
);
|
|
continue;
|
|
}
|
|
// Sync observed ourselves so the next plan_reconcile
|
|
// does not replay this action. CLI does not push a
|
|
// config-update notification after set_config_option.
|
|
let mut session = self.session.write().await;
|
|
session.apply_observed_config(key, value);
|
|
self.commit_session_changes(&mut session).await;
|
|
}
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn reconcile_action_equality() {
|
|
let a = ReconcileAction::SetMode {
|
|
mode: ModeId::new("plan"),
|
|
};
|
|
let b = ReconcileAction::SetMode {
|
|
mode: ModeId::new("plan"),
|
|
};
|
|
assert_eq!(a, b);
|
|
}
|
|
}
|