Skip to content

Commit b4dd5dd

Browse files
authored
feat(acp): expose current model in SessionInfoUpdate and session/list (#2435) (#2453)
Each in-memory SessionInfo in session/list now carries meta.currentModel. After session/set_config_option with configId=model, a SessionInfoUpdate notification is sent with meta.currentModel in addition to the existing ConfigOptionUpdate. Same notification is emitted after set_session_model.
1 parent 06958c2 commit b4dd5dd

File tree

4 files changed

+152
-3
lines changed

4 files changed

+152
-3
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
88

99
### Added
1010

11+
- feat(acp): expose current model in `session/list` and emit `SessionInfoUpdate` on model change — each in-memory `SessionInfo` now carries `meta.currentModel`; after `session/set_config_option` with `configId=model` a `SessionInfoUpdate` notification with `meta.currentModel` is sent in addition to the existing `ConfigOptionUpdate`; same notification is sent after `session/set_session_model` (closes #2435)
1112
- feat(memory): Memex tool output archive — before compaction, `ToolOutput` bodies in the compaction range are saved to `tool_overflow` with `archive_type = 'archive'`; archived UUIDs are appended as a postfix after LLM summarization so references survive compaction; controlled by `[memory.compression] archive_tool_outputs = false`; archives are excluded from the short-lived cleanup job via `archive_type` column (migration 054, closes #2432)
1213
- feat(memory): ACON per-category compression guidelines — `compression_failure_pairs` now stores a `category` column (`tool_output`, `assistant_reasoning`, `user_context`, `unknown`); the compression guidelines table gains a `category` column with `UNIQUE(version, category)` constraint; the `compression_guidelines` updater can now maintain per-category guideline documents when `categorized_guidelines = true`; failure category is classified from the compaction summary content before calling the LLM (migration 054, closes #2433)
1314
- feat(memory): RL-based admission control — new `AdmissionStrategy` enum with `heuristic` (default) and `rl` variants; `admission_training_data` table records all messages seen by A-MAC (admitted and rejected) to eliminate survivorship bias; `was_recalled` flag is set by `SemanticMemory::recall()` to provide positive training signal; lightweight logistic regression model in `admission_rl.rs` replaces the LLM `future_utility` factor when enough samples are available; weights persisted in `admission_rl_weights` table; controlled by `[memory.admission] admission_strategy`, `rl_min_samples = 500`, `rl_retrain_interval_secs = 3600` (migration 055, closes #2416)

crates/zeph-acp/src/agent/helpers.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,16 @@ pub(super) fn tool_kind_from_name(name: &str) -> acp::ToolKind {
7575
}
7676
}
7777

78+
/// Build a `Meta` map carrying the current model name under the `"currentModel"` key.
79+
pub(super) fn model_meta(model: &str) -> serde_json::Map<String, serde_json::Value> {
80+
let mut map = serde_json::Map::new();
81+
map.insert(
82+
"currentModel".to_owned(),
83+
serde_json::Value::String(model.to_owned()),
84+
);
85+
map
86+
}
87+
7888
pub(super) const DEFAULT_MODE_ID: &str = "code";
7989

8090
/// MIME type used by Zed IDE to deliver LSP diagnostics as embedded resource blocks.

crates/zeph-acp/src/agent/mod.rs

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1129,8 +1129,10 @@ impl acp::Agent for ZephAcpAgent {
11291129
{
11301130
return None;
11311131
}
1132+
let meta = model_meta(&entry.current_model.borrow());
11321133
let mut info = acp::SessionInfo::new(session_id.clone(), working_dir)
1133-
.updated_at(entry.created_at.to_rfc3339());
1134+
.updated_at(entry.created_at.to_rfc3339())
1135+
.meta(meta);
11341136
if let Some(ref t) = *entry.title.borrow() {
11351137
info = info.title(t.clone());
11361138
}
@@ -1403,11 +1405,24 @@ impl acp::Agent for ZephAcpAgent {
14031405
// deadlocks in callers that do not drain notifications.
14041406
let update =
14051407
acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate::new(vec![option]));
1406-
let notification = acp::SessionNotification::new(args.session_id, update);
1408+
let notification = acp::SessionNotification::new(args.session_id.clone(), update);
14071409
let (tx, _rx) = oneshot::channel();
14081410
if self.notify_tx.send((notification, tx)).is_err() {
14091411
tracing::warn!("failed to send ConfigOptionUpdate notification: channel closed");
14101412
}
1413+
1414+
// When the model config changes, also emit a SessionInfoUpdate so IDE clients
1415+
// that track session metadata learn about the new model immediately.
1416+
if config_id.as_ref() == "model" {
1417+
let info_update = acp::SessionUpdate::SessionInfoUpdate(
1418+
acp::SessionInfoUpdate::new().meta(model_meta(&current_model)),
1419+
);
1420+
let info_notification = acp::SessionNotification::new(args.session_id, info_update);
1421+
let (tx2, _rx2) = oneshot::channel();
1422+
if self.notify_tx.send((info_notification, tx2)).is_err() {
1423+
tracing::warn!("failed to send SessionInfoUpdate notification: channel closed");
1424+
}
1425+
}
14111426
}
14121427

14131428
Ok(acp::SetSessionConfigOptionResponse::new(config_options))
@@ -1487,6 +1502,16 @@ impl acp::Agent for ZephAcpAgent {
14871502
"ACP session model switched via set_session_model"
14881503
);
14891504

1505+
// Notify IDE clients about the new model via SessionInfoUpdate.
1506+
let info_update = acp::SessionUpdate::SessionInfoUpdate(
1507+
acp::SessionInfoUpdate::new().meta(model_meta(model_id)),
1508+
);
1509+
let notification = acp::SessionNotification::new(args.session_id, info_update);
1510+
let (tx, _rx) = oneshot::channel();
1511+
if self.notify_tx.send((notification, tx)).is_err() {
1512+
tracing::warn!("failed to send SessionInfoUpdate notification: channel closed");
1513+
}
1514+
14901515
Ok(acp::SetSessionModelResponse::new())
14911516
}
14921517
}
@@ -2208,7 +2233,7 @@ impl ZephAcpAgent {
22082233
pub(super) mod helpers;
22092234
use helpers::{
22102235
DEFAULT_MODE_ID, DIAGNOSTICS_MIME_TYPE, build_available_commands, build_config_options,
2211-
build_mode_state, format_diagnostics_block, loopback_event_to_updates, mime_to_ext,
2236+
build_mode_state, format_diagnostics_block, loopback_event_to_updates, mime_to_ext, model_meta,
22122237
session_update_to_event, xml_escape,
22132238
};
22142239

crates/zeph-acp/src/agent/tests.rs

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3804,6 +3804,119 @@ async fn close_session_unknown_id_is_ok() {
38043804
.await;
38053805
}
38063806

3807+
#[tokio::test]
3808+
async fn list_sessions_includes_model_meta() {
3809+
let local = tokio::task::LocalSet::new();
3810+
local
3811+
.run_until(async {
3812+
let (tx, _rx) = mpsc::unbounded_channel();
3813+
let conn_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
3814+
let models = shared_models(vec!["ollama:llama3".to_owned()]);
3815+
let factory: ProviderFactory = Arc::new(|_key| None);
3816+
let agent = ZephAcpAgent::new(make_spawner(), tx, conn_slot, 4, 1800, None)
3817+
.with_provider_factory(factory, Arc::clone(&models));
3818+
let resp = agent
3819+
.new_session(acp::NewSessionRequest::new(std::path::PathBuf::from(".")))
3820+
.await
3821+
.unwrap();
3822+
let sid = resp.session_id.clone();
3823+
3824+
let list_resp = agent
3825+
.list_sessions(acp::ListSessionsRequest::new())
3826+
.await
3827+
.unwrap();
3828+
3829+
let info = list_resp
3830+
.sessions
3831+
.iter()
3832+
.find(|s| s.session_id == sid)
3833+
.expect("session must appear in list");
3834+
3835+
let meta = info.meta.as_ref().expect("meta must be present");
3836+
assert!(
3837+
meta.contains_key("currentModel"),
3838+
"meta must contain 'currentModel'"
3839+
);
3840+
assert_eq!(
3841+
meta["currentModel"],
3842+
serde_json::Value::String("ollama:llama3".to_owned())
3843+
);
3844+
})
3845+
.await;
3846+
}
3847+
3848+
#[tokio::test]
3849+
async fn set_config_option_model_emits_session_info_update() {
3850+
let local = tokio::task::LocalSet::new();
3851+
local
3852+
.run_until(async {
3853+
let (tx, mut notify_rx) = mpsc::unbounded_channel();
3854+
let conn_slot = std::rc::Rc::new(std::cell::RefCell::new(None));
3855+
let models = shared_models(vec!["ollama:llama3".to_owned()]);
3856+
let factory: ProviderFactory = Arc::new(|key: &str| {
3857+
if key == "ollama:llama3" {
3858+
Some(zeph_llm::any::AnyProvider::Ollama(
3859+
zeph_llm::ollama::OllamaProvider::new(
3860+
"http://localhost:11434",
3861+
"llama3".into(),
3862+
"nomic-embed-text".into(),
3863+
),
3864+
))
3865+
} else {
3866+
None
3867+
}
3868+
});
3869+
let agent = ZephAcpAgent::new(make_spawner(), tx, conn_slot, 4, 1800, None)
3870+
.with_provider_factory(factory, Arc::clone(&models));
3871+
let resp = agent
3872+
.new_session(acp::NewSessionRequest::new(std::path::PathBuf::from(".")))
3873+
.await
3874+
.unwrap();
3875+
let sid = resp.session_id.clone();
3876+
3877+
// Drain any notifications from new_session.
3878+
while notify_rx.try_recv().is_ok() {}
3879+
3880+
agent
3881+
.set_session_config_option(acp::SetSessionConfigOptionRequest::new(
3882+
sid.clone(),
3883+
"model",
3884+
"ollama:llama3",
3885+
))
3886+
.await
3887+
.unwrap();
3888+
3889+
// Collect all notifications sent.
3890+
let mut updates = vec![];
3891+
while let Ok((notif, _ack)) = notify_rx.try_recv() {
3892+
updates.push(notif.update);
3893+
}
3894+
3895+
let has_config_update = updates
3896+
.iter()
3897+
.any(|u| matches!(u, acp::SessionUpdate::ConfigOptionUpdate(_)));
3898+
assert!(has_config_update, "ConfigOptionUpdate must be sent");
3899+
3900+
let session_info_update = updates.iter().find_map(|u| {
3901+
if let acp::SessionUpdate::SessionInfoUpdate(siu) = u {
3902+
Some(siu)
3903+
} else {
3904+
None
3905+
}
3906+
});
3907+
let siu = session_info_update.expect("SessionInfoUpdate must be sent on model change");
3908+
let meta = siu
3909+
.meta
3910+
.as_ref()
3911+
.expect("SessionInfoUpdate must carry meta");
3912+
assert_eq!(
3913+
meta["currentModel"],
3914+
serde_json::Value::String("ollama:llama3".to_owned())
3915+
);
3916+
})
3917+
.await;
3918+
}
3919+
38073920
#[cfg(feature = "unstable-session-close")]
38083921
#[tokio::test]
38093922
async fn close_session_signals_cancel() {

0 commit comments

Comments
 (0)