Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changelog.d/332.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Sessions now restore plugin state and todo lists across restarts in UI, SDK, ACP, print, and agent modes.
3 changes: 2 additions & 1 deletion n00n-acp/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ pub mod translate;
use std::path::PathBuf;
use std::sync::Arc;

use n00n_agent::headless::InteractiveHandle;
use n00n_agent::headless::{InteractiveHandle, SessionStatePersistence};
use n00n_agent::prompt::ResolvedSlots;
use n00n_agent::{AgentConfig, PermissionsConfig};
use n00n_providers::model::Model;
Expand All @@ -27,6 +27,7 @@ pub struct AcpParams {
pub initial_wd: PathBuf,
pub mcp_handle: Option<n00n_agent::McpHandle>,
pub prompt_slots: Arc<ResolvedSlots>,
pub state_persistence: Option<Arc<dyn SessionStatePersistence>>,
pub yolo: bool,
pub session_daemon_register: Option<SessionDaemonRegister>,
}
Expand Down
118 changes: 83 additions & 35 deletions n00n-acp/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ struct SessionState {
plan_path: Option<PathBuf>,
current_model: String,
pending_prompt: PendingPrompt,
event_pump: smol::Task<()>,
_daemon: Option<crate::SessionDaemonGuard>,
}

Expand Down Expand Up @@ -116,14 +117,15 @@ pub async fn serve(params: AcpParams) -> color_eyre::Result<()> {
handle_incoming_response(&server, &raw);
} else if let Some(method) = raw.get("method").and_then(Value::as_str) {
match id {
Some(id) => handle_request(&mut server, method, id, &raw, &params),
Some(id) => handle_request(&mut server, method, id, &raw, &params).await,
None => handle_notification(&server, method),
}
} else if let Some(id) = id {
server.respond(id, Err(AcpError::invalid_request()));
}
}

retire_session(&mut server).await;
drop(server);
writer_task.await;

Expand All @@ -134,41 +136,19 @@ fn request_id(v: &Value) -> RequestId {
serde_json::from_value(v.clone()).map_or(RequestId::Null, std::convert::identity)
}

fn handle_request(srv: &mut Server, method: &str, id: RequestId, raw: &Value, params: &AcpParams) {
async fn handle_request(
srv: &mut Server,
method: &str,
id: RequestId,
raw: &Value,
params: &AcpParams,
) {
let result = match method {
"initialize" => Ok(AgentResponse::InitializeResponse(
methods::initialize_response(),
)),
"session/new" => parse_params::<NewSessionRequest>(raw).map(|req| {
let handle = spawn_session(params, req.cwd, None, Vec::new());
let spec = params.model.spec();
let resp = methods::new_session_response(handle.session_id.as_str())
.config_options(vec![methods::model_config_option(&spec, &srv.model_specs)]);
install_session(srv, handle, spec, AgentMode::Build, None, params);
AgentResponse::NewSessionResponse(resp)
}),
"session/load" => parse_params::<LoadSessionRequest>(raw).and_then(|req| {
let session_ref: SessionRef =
req.session_id.0.parse().map_err(|_| {
AcpError::resource_not_found(Some(req.session_id.0.to_string()))
})?;
let storage = n00n_storage::StateDir::resolve()
.map_err(|e| AcpError::internal_error().data(json_str(&e)))?;
let stored = load_session_from(&storage, session_ref.id())?;
let (current_mode, plan_path) = mode_and_plan_from_stored(&storage, &stored.meta)
.map_err(|e| AcpError::internal_error().data(json_str(&e)))?;
let history = stored.messages;
let sid = SessionId::from(session_ref.to_string());
for update in translate::replay_history(&history) {
session_update(&srv.out_tx, &sid, update);
}
let handle = spawn_session(params, req.cwd, Some(session_ref), history);
let spec = params.model.spec();
let resp = methods::load_session_response()
.config_options(vec![methods::model_config_option(&spec, &srv.model_specs)]);
install_session(srv, handle, spec, current_mode, plan_path, params);
Ok(AgentResponse::LoadSessionResponse(resp))
}),
"session/new" => handle_new_session(srv, raw, params).await,
"session/load" => handle_load_session(srv, raw, params).await,
"session/prompt" => match handle_prompt(srv, raw, &id) {
Ok(()) => return,
Err(e) => Err(e),
Expand All @@ -180,6 +160,73 @@ fn handle_request(srv: &mut Server, method: &str, id: RequestId, raw: &Value, pa
srv.respond(id, result);
}

async fn handle_new_session(
srv: &mut Server,
raw: &Value,
params: &AcpParams,
) -> Result<AgentResponse, AcpError> {
let req = parse_params::<NewSessionRequest>(raw)?;
retire_session(srv).await;
let handle = spawn_session(params, req.cwd, None, Vec::new());
let spec = params.model.spec();
let resp = methods::new_session_response(handle.session_id.as_str())
.config_options(vec![methods::model_config_option(&spec, &srv.model_specs)]);
install_session(srv, handle, spec, AgentMode::Build, None, params);
Ok(AgentResponse::NewSessionResponse(resp))
}

async fn handle_load_session(
srv: &mut Server,
raw: &Value,
params: &AcpParams,
) -> Result<AgentResponse, AcpError> {
let req = parse_params::<LoadSessionRequest>(raw)?;
let session_ref: SessionRef = req
.session_id
.0
.parse()
.map_err(|_| AcpError::resource_not_found(Some(req.session_id.0.to_string())))?;
let storage = n00n_storage::StateDir::resolve()
.map_err(|error| AcpError::internal_error().data(json_str(&error)))?;
let stored = load_session_from(&storage, session_ref.id())?;
let (current_mode, plan_path) = mode_and_plan_from_stored(&storage, &stored.meta)
.map_err(|error| AcpError::internal_error().data(json_str(&error)))?;
let history = stored.messages;
let sid = SessionId::from(session_ref.to_string());
for update in translate::replay_history(&history) {
session_update(&srv.out_tx, &sid, update);
}
retire_session(srv).await;
let handle = spawn_session(params, req.cwd, Some(session_ref), history);
let spec = params.model.spec();
let resp = methods::load_session_response()
.config_options(vec![methods::model_config_option(&spec, &srv.model_specs)]);
install_session(srv, handle, spec, current_mode, plan_path, params);
Ok(AgentResponse::LoadSessionResponse(resp))
}

async fn retire_session(srv: &mut Server) {
let Some(session) = srv.session.take() else {
return;
};
let SessionState {
handle,
event_pump,
pending_prompt,
..
} = session;
if let Some((id, _)) = take_pending(&pending_prompt) {
let response = PromptResponse::new(StopReason::Cancelled);
send(
&srv.out_tx,
Response::new(id, Ok(AgentResponse::PromptResponse(response))),
);
}
let _ = handle.cancel_tx.try_send(());
event_pump.cancel().await;
handle.task.cancel().await;
}

fn spawn_session(
params: &AcpParams,
cwd: PathBuf,
Expand All @@ -193,6 +240,7 @@ fn spawn_session(
timeouts: params.timeouts,
openai_options: params.openai_options,
prompt_slots: Arc::clone(&params.prompt_slots),
state_persistence: params.state_persistence.clone(),
excluded_tools: Vec::new(),
mcp_handle: params.mcp_handle.clone(),
initial_wd: cwd,
Expand All @@ -215,7 +263,7 @@ fn install_session(
params: &AcpParams,
) {
let pending = Arc::new(Mutex::new(PendingPromptState::default()));
start_event_pump(
let event_pump = start_event_pump(
handle.event_rx.clone(),
handle.session_id.clone(),
srv.out_tx.clone(),
Expand All @@ -232,6 +280,7 @@ fn install_session(
plan_path,
current_model,
pending_prompt: pending,
event_pump,
_daemon: daemon,
});
}
Expand Down Expand Up @@ -414,7 +463,7 @@ fn start_event_pump(
session_id: SessionRef,
out_tx: Sender<Value>,
pending: PendingPrompt,
) {
) -> smol::Task<()> {
smol::spawn(async move {
let sid = SessionId::from(session_id.to_string());
let mut next_request_id = FIRST_OUTGOING_REQUEST_ID;
Expand Down Expand Up @@ -484,7 +533,6 @@ fn start_event_pump(
session_update(&out_tx, &sid, update);
}
})
.detach();
}

fn take_pending(pending: &PendingPrompt) -> Option<(RequestId, bool)> {
Expand Down
Loading
Loading