use super::*;
impl AcpServer {
#[cfg(feature = "hostlib")]
pub(super) fn handle_session_fs_mode(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = params
.get("sessionId")
.or_else(|| params.get("session_id"))
.and_then(serde_json::Value::as_str)
else {
self.send_error(id, -32602, "session/fs_mode requires sessionId");
return;
};
let Some(mode_raw) = params.get("mode").and_then(serde_json::Value::as_str) else {
self.send_error(id, -32602, "session/fs_mode requires mode");
return;
};
let mode = match mode_raw {
"immediate" => harn_hostlib::fs::FsMode::Immediate,
"staged" => harn_hostlib::fs::FsMode::Staged,
other => {
self.send_error(
id,
-32602,
&format!("session/fs_mode mode must be immediate or staged, got {other}"),
);
return;
}
};
let Some(cwd) = self
.sessions
.get(session_id)
.map(|session| session.cwd.clone())
else {
self.send_error(id, -32602, &format!("Unknown session: {session_id}"));
return;
};
match harn_hostlib::fs::set_mode(session_id, mode, Some(&cwd)) {
Ok(result) => {
self.send_response(
id,
serde_json::json!({
"previousMode": result.previous_mode.as_str(),
"mode": mode.as_str(),
}),
);
self.emit_staged_writes_update(session_id);
}
Err(error) => self.send_error(id, -32000, &error.to_string()),
}
}
#[cfg(not(feature = "hostlib"))]
pub(super) fn handle_session_fs_mode(
&mut self,
id: &serde_json::Value,
_params: &serde_json::Value,
) {
self.send_error(id, -32601, "session/fs_mode requires the hostlib feature");
}
#[cfg(feature = "hostlib")]
pub(super) fn handle_session_fs_commit_staged(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = session_id_param(params) else {
self.send_error(id, -32602, "session/fs_commit_staged requires sessionId");
return;
};
if !self.sessions.contains_key(session_id.as_str()) {
self.send_error(id, -32602, &format!("Unknown session: {session_id}"));
return;
}
let paths = staged_fs_paths_param(params);
match harn_hostlib::fs::commit_staged(session_id.as_str(), &paths) {
Ok(result) => {
harn_vm::agent_sessions::invalidate_redo(session_id.as_str());
self.send_response(
id,
serde_json::json!({
"committedPaths": result.committed_paths,
"failedPathsWithReasons": result
.failed_paths_with_reasons
.into_iter()
.map(|(path, reason)| serde_json::json!({
"path": path,
"reason": reason,
}))
.collect::<Vec<_>>(),
}),
);
self.emit_staged_writes_update(session_id.as_str());
}
Err(error) => self.send_error(id, -32000, &error.to_string()),
}
}
#[cfg(not(feature = "hostlib"))]
pub(super) fn handle_session_fs_commit_staged(
&mut self,
id: &serde_json::Value,
_params: &serde_json::Value,
) {
self.send_error(
id,
-32601,
"session/fs_commit_staged requires the hostlib feature",
);
}
#[cfg(feature = "hostlib")]
pub(super) fn handle_session_fs_discard_staged(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = session_id_param(params) else {
self.send_error(id, -32602, "session/fs_discard_staged requires sessionId");
return;
};
if !self.sessions.contains_key(session_id.as_str()) {
self.send_error(id, -32602, &format!("Unknown session: {session_id}"));
return;
}
let paths = staged_fs_paths_param(params);
match harn_hostlib::fs::discard_staged(session_id.as_str(), &paths) {
Ok(result) => {
harn_vm::agent_sessions::invalidate_redo(session_id.as_str());
self.send_response(
id,
serde_json::json!({
"discardedPaths": result.discarded_paths,
}),
);
self.emit_staged_writes_update(session_id.as_str());
}
Err(error) => self.send_error(id, -32000, &error.to_string()),
}
}
#[cfg(not(feature = "hostlib"))]
pub(super) fn handle_session_fs_discard_staged(
&mut self,
id: &serde_json::Value,
_params: &serde_json::Value,
) {
self.send_error(
id,
-32601,
"session/fs_discard_staged requires the hostlib feature",
);
}
#[cfg(feature = "hostlib")]
pub(super) fn handle_session_restore_tool_call(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = params
.get("sessionId")
.or_else(|| params.get("session_id"))
.and_then(serde_json::Value::as_str)
else {
self.send_error(id, -32602, "session/restore_tool_call requires sessionId");
return;
};
let Some(tool_call_id) = params
.get("toolCallId")
.or_else(|| params.get("tool_call_id"))
.and_then(serde_json::Value::as_str)
else {
self.send_error(id, -32602, "session/restore_tool_call requires toolCallId");
return;
};
if !self.sessions.contains_key(session_id) {
self.send_error(id, -32602, &format!("Unknown session: {session_id}"));
return;
}
let paths = params
.get("paths")
.and_then(serde_json::Value::as_array)
.map(|values| {
values
.iter()
.filter_map(serde_json::Value::as_str)
.map(str::to_string)
.collect::<Vec<_>>()
})
.unwrap_or_default();
match harn_hostlib::fs_snapshot::restore(session_id, tool_call_id, &paths) {
Ok(result) => {
harn_vm::agent_sessions::invalidate_redo(session_id);
self.send_response(
id,
serde_json::json!({
"toolCallId": &result.snapshot_id,
"restoredPaths": &result.restored_paths,
"skippedPathsWithReasons": result
.skipped_paths_with_reasons
.iter()
.map(|(path, reason)| serde_json::json!({
"path": path,
"reason": reason,
}))
.collect::<Vec<_>>(),
}),
);
let mut update = serde_json::json!({
"sessionUpdate": "tool_call_update",
"toolCallId": &result.snapshot_id,
"status": "restored",
});
let mut harn_meta = serde_json::Map::new();
harn_meta.insert(
"kind".to_string(),
serde_json::Value::String("tool_call_restored".to_string()),
);
harn_meta.insert(
"restoredPaths".to_string(),
serde_json::to_value(&result.restored_paths).unwrap_or_default(),
);
if !result.skipped_paths_with_reasons.is_empty() {
harn_meta.insert(
"skippedPathsWithReasons".to_string(),
serde_json::to_value(
result
.skipped_paths_with_reasons
.iter()
.map(|(path, reason)| {
serde_json::json!({
"path": path,
"reason": reason,
})
})
.collect::<Vec<_>>(),
)
.unwrap_or_default(),
);
}
events::merge_harn_meta(&mut update, harn_meta);
self.send_notification(
"session/update",
serde_json::json!({
"sessionId": session_id,
"update": update,
}),
);
}
Err(error) => self.send_error(id, -32000, &error.to_string()),
}
}
#[cfg(not(feature = "hostlib"))]
pub(super) fn handle_session_restore_tool_call(
&mut self,
id: &serde_json::Value,
_params: &serde_json::Value,
) {
self.send_error(
id,
-32601,
"session/restore_tool_call requires the hostlib feature",
);
}
#[cfg(feature = "hostlib")]
pub(super) fn restore_fs_snapshots(
&self,
session_id: &str,
snapshot_ids: &[String],
reverse: bool,
) -> Result<Vec<serde_json::Value>, String> {
let mut ids = snapshot_ids.to_vec();
if reverse {
ids.reverse();
}
let mut restored = Vec::new();
for snapshot_id in ids {
let result = harn_hostlib::fs_snapshot::restore(session_id, &snapshot_id, &[])
.map_err(|error| error.to_string())?;
restored.push(serde_json::json!({
"snapshotId": result.snapshot_id,
"restoredPaths": result.restored_paths,
"skippedPathsWithReasons": result
.skipped_paths_with_reasons
.into_iter()
.map(|(path, reason)| serde_json::json!({
"path": path,
"reason": reason,
}))
.collect::<Vec<_>>(),
}));
}
Ok(restored)
}
#[cfg(feature = "hostlib")]
pub(super) fn capture_redo_snapshots(
&self,
session_id: &str,
checkpoint_id: &str,
rollback_snapshot_ids: &[String],
) -> Result<Vec<String>, String> {
let Some(cwd) = self
.sessions
.get(session_id)
.map(|session| session.cwd.clone())
else {
return Err(format!("Unknown session: {session_id}"));
};
let summaries = harn_hostlib::fs_snapshot::list_snapshots(session_id)
.map_err(|error| error.to_string())?;
let by_id: HashMap<String, harn_hostlib::fs_snapshot::SnapshotSummary> = summaries
.into_iter()
.map(|summary| (summary.snapshot_id.clone(), summary))
.collect();
let mut redo_ids = Vec::new();
for snapshot_id in rollback_snapshot_ids {
let Some(summary) = by_id.get(snapshot_id) else {
continue;
};
if summary.captured_paths.is_empty() {
continue;
}
let redo_id = format!("{checkpoint_id}:redo:{snapshot_id}");
harn_hostlib::fs_snapshot::snapshot(
session_id,
&redo_id,
&summary.captured_paths,
Some(&cwd),
)
.map_err(|error| error.to_string())?;
redo_ids.push(redo_id);
}
Ok(redo_ids)
}
pub(super) fn checkpoint_response(
&self,
outcome: harn_vm::agent_sessions::SessionCheckpointOutcome,
fs_restores: Vec<serde_json::Value>,
) -> serde_json::Value {
serde_json::json!({
"status": outcome.status,
"checkpointId": outcome.checkpoint.checkpoint_id,
"beforeMessageCount": outcome.checkpoint.before_message_count,
"afterMessageCount": outcome.checkpoint.after_message_count,
"fsSnapshotIds": outcome.checkpoint.fs_snapshot_ids,
"redoFsSnapshotIds": outcome.redo_fs_snapshot_ids,
"fsRestores": fs_restores,
})
}
pub(super) fn send_checkpoint_error(
&self,
id: &serde_json::Value,
code: i64,
status: &'static str,
message: &str,
) {
self.send_error_with_data(id, code, message, serde_json::json!({ "status": status }));
}
pub(super) fn handle_session_rollback(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = session_id_param(params) else {
self.send_checkpoint_error(
id,
-32602,
"invalid_params",
"session/rollback requires sessionId",
);
return;
};
let Some(session) = self.sessions.get(&session_id) else {
self.send_checkpoint_error(
id,
-32602,
"unknown_session",
&format!("Unknown session: {session_id}"),
);
return;
};
if session.host_bridge.is_some() {
self.send_checkpoint_error(
id,
-32000,
"prompt_active",
"session/rollback rejected: prompt is active",
);
return;
}
let plan = match harn_vm::agent_sessions::rollback_plan(&session_id) {
Ok(plan) => plan,
Err(error) => {
let status = harn_vm::agent_sessions::checkpoint_status_name(error);
self.send_checkpoint_error(id, -32000, status, status);
return;
}
};
#[cfg(not(feature = "hostlib"))]
let _ = &plan;
#[cfg(feature = "hostlib")]
let redo_fs_snapshot_ids = match self.capture_redo_snapshots(
&session_id,
&plan.checkpoint_id,
&plan.fs_snapshot_ids,
) {
Ok(ids) => ids,
Err(message) => {
self.send_checkpoint_error(id, -32000, "fs_snapshot_error", &message);
return;
}
};
#[cfg(not(feature = "hostlib"))]
let redo_fs_snapshot_ids = Vec::new();
#[cfg(feature = "hostlib")]
let fs_restores = match self.restore_fs_snapshots(&session_id, &plan.fs_snapshot_ids, true)
{
Ok(restores) => restores,
Err(message) => {
self.send_checkpoint_error(id, -32000, "fs_restore_error", &message);
return;
}
};
#[cfg(not(feature = "hostlib"))]
let fs_restores = Vec::new();
let outcome = match harn_vm::agent_sessions::rollback_last_completed_turn(
&session_id,
redo_fs_snapshot_ids,
) {
Ok(outcome) => outcome,
Err(error) => {
let status = harn_vm::agent_sessions::checkpoint_status_name(error);
self.send_checkpoint_error(id, -32000, status, status);
return;
}
};
self.send_notification(
"session/update",
serde_json::json!({
"sessionId": session_id,
"update": {
"sessionUpdate": "session_rollback",
"checkpointId": outcome.checkpoint.checkpoint_id,
"status": outcome.status,
},
}),
);
self.send_response(id, self.checkpoint_response(outcome, fs_restores));
}
pub(super) fn handle_session_redo(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = session_id_param(params) else {
self.send_checkpoint_error(
id,
-32602,
"invalid_params",
"session/redo requires sessionId",
);
return;
};
let Some(session) = self.sessions.get(&session_id) else {
self.send_checkpoint_error(
id,
-32602,
"unknown_session",
&format!("Unknown session: {session_id}"),
);
return;
};
if session.host_bridge.is_some() {
self.send_checkpoint_error(
id,
-32000,
"prompt_active",
"session/redo rejected: prompt is active",
);
return;
}
let plan = match harn_vm::agent_sessions::redo_plan(&session_id) {
Ok(plan) => plan,
Err(error) => {
let status = harn_vm::agent_sessions::checkpoint_status_name(error);
self.send_checkpoint_error(id, -32000, status, status);
return;
}
};
#[cfg(not(feature = "hostlib"))]
let _ = &plan;
#[cfg(feature = "hostlib")]
let fs_restores = match self.restore_fs_snapshots(&session_id, &plan.fs_snapshot_ids, false)
{
Ok(restores) => restores,
Err(message) => {
self.send_checkpoint_error(id, -32000, "fs_restore_error", &message);
return;
}
};
#[cfg(not(feature = "hostlib"))]
let fs_restores = Vec::new();
let outcome = match harn_vm::agent_sessions::redo_last_rollback(&session_id) {
Ok(outcome) => outcome,
Err(error) => {
let status = harn_vm::agent_sessions::checkpoint_status_name(error);
self.send_checkpoint_error(id, -32000, status, status);
return;
}
};
self.send_notification(
"session/update",
serde_json::json!({
"sessionId": session_id,
"update": {
"sessionUpdate": "session_redo",
"checkpointId": outcome.checkpoint.checkpoint_id,
"status": outcome.status,
},
}),
);
self.send_response(id, self.checkpoint_response(outcome, fs_restores));
}
pub(super) fn handle_session_set_mode(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = params
.get("sessionId")
.or_else(|| params.get("session_id"))
.and_then(serde_json::Value::as_str)
else {
self.send_error(id, -32602, "session/set_mode requires sessionId");
return;
};
let Some(mode_id) = params
.get("modeId")
.or_else(|| params.get("mode_id"))
.and_then(serde_json::Value::as_str)
else {
self.send_error(id, -32602, "session/set_mode requires modeId");
return;
};
match self.set_session_mode(session_id, mode_id) {
Ok(changed) => {
self.send_response(id, serde_json::json!({}));
if changed {
self.emit_current_mode_update(session_id, mode_id);
self.emit_config_option_update(session_id, mode_id);
}
}
Err(message) => self.send_error(id, -32602, &message),
}
}
pub(super) fn handle_session_set_config_option(
&mut self,
id: &serde_json::Value,
params: &serde_json::Value,
) {
let Some(session_id) = params.get("sessionId").and_then(serde_json::Value::as_str) else {
self.send_error(id, -32602, "session/set_config_option requires sessionId");
return;
};
let Some(config_id) = params.get("configId").and_then(serde_json::Value::as_str) else {
self.send_error(id, -32602, "session/set_config_option requires configId");
return;
};
let Some(value) = params.get("value").and_then(serde_json::Value::as_str) else {
self.send_error(id, -32602, "session/set_config_option requires value");
return;
};
let session_id = session_id.to_string();
match config_id {
"mode" => self.apply_set_mode_config_option(id, &session_id, value),
"model" => self.apply_set_model_config_option(id, &session_id, value),
"thought_level" | "reasoning_policy" => {
self.apply_set_reasoning_policy_config_option(id, &session_id, value);
}
"budget" => self.apply_set_budget_config_option(id, &session_id, value),
other => self.send_error(
id,
-32602,
&format!(
"Unknown config option '{other}'. Available: mode, model, thought_level, budget"
),
),
}
}
pub(super) fn apply_set_mode_config_option(
&mut self,
id: &serde_json::Value,
session_id: &str,
mode_id: &str,
) {
match self.set_session_mode(session_id, mode_id) {
Ok(changed) => {
self.send_response(
id,
serde_json::json!({
"configOptions": self.config_options_for_session(session_id, mode_id),
}),
);
if changed {
self.emit_current_mode_update(session_id, mode_id);
self.emit_config_option_update(session_id, mode_id);
}
}
Err(message) => self.send_error(id, -32602, &message),
}
}
pub(super) fn apply_set_model_config_option(
&mut self,
id: &serde_json::Value,
session_id: &str,
raw_value: &str,
) {
let normalized = match modes::validate_model_selector(raw_value) {
Ok(value) => value,
Err(message) => {
self.send_error(id, -32602, &message);
return;
}
};
match self.set_session_model(session_id, normalized) {
Ok(changed) => {
let current_mode_id = self
.sessions
.get(session_id)
.map(|session| session.current_mode_id.clone())
.unwrap_or_else(|| modes::DEFAULT_MODE_ID.to_string());
self.send_response(
id,
serde_json::json!({
"configOptions": self.config_options_for_session(session_id, ¤t_mode_id),
}),
);
if changed {
self.emit_config_option_update(session_id, ¤t_mode_id);
}
}
Err(message) => self.send_error(id, -32602, &message),
}
}
pub(super) fn apply_set_reasoning_policy_config_option(
&mut self,
id: &serde_json::Value,
session_id: &str,
raw_value: &str,
) {
let normalized = match modes::validate_reasoning_policy_selector(raw_value) {
Ok(value) => value,
Err(message) => {
self.send_error(id, -32602, &message);
return;
}
};
match self.set_session_reasoning_policy(session_id, normalized) {
Ok(changed) => {
let current_mode_id = self
.sessions
.get(session_id)
.map(|session| session.current_mode_id.clone())
.unwrap_or_else(|| modes::DEFAULT_MODE_ID.to_string());
self.send_response(
id,
serde_json::json!({
"configOptions": self.config_options_for_session(session_id, ¤t_mode_id),
}),
);
if changed {
self.emit_config_option_update(session_id, ¤t_mode_id);
}
}
Err(message) => self.send_error(id, -32602, &message),
}
}
pub(super) fn apply_set_budget_config_option(
&mut self,
id: &serde_json::Value,
session_id: &str,
raw_value: &str,
) {
let budget = match parse_budget_config_value(raw_value) {
Ok(value) => value,
Err(message) => {
self.send_error(id, -32602, &message);
return;
}
};
match self.set_session_budget(session_id, budget) {
Ok(changed) => {
let current_mode_id = self
.sessions
.get(session_id)
.map(|session| session.current_mode_id.clone())
.unwrap_or_else(|| modes::DEFAULT_MODE_ID.to_string());
self.send_response(
id,
serde_json::json!({
"configOptions": self.config_options_for_session(session_id, ¤t_mode_id),
}),
);
if changed {
self.emit_config_option_update(session_id, ¤t_mode_id);
}
}
Err(message) => self.send_error(id, -32602, &message),
}
}
}