use std::sync::Arc; use tokio::sync::mpsc; use crate::{ cursor::prompting::PromptCompiler, cursor::{ blob_sync::BlobSynchronizer, checkpoint::CheckpointBuilder, context_sync::RequestContextSynchronizer, proto::agent::v1 as pb, request, session::CursorSession, tools::{ codec, result::tool_result_channel, runtime::CursorToolRuntime, ClientToolEvent, ToolDispatcher, }, }, provider::Provider, run::{RunActor, RunRegistry}, store::Store, }; use super::{inbox::OrderedInbox, CursorCommand, CursorSessionHandle}; pub struct CursorActor; #[derive(Clone)] pub(crate) struct RunDependencies { pub store: Store, pub provider: Arc, pub compiler: PromptCompiler, pub run_registry: RunRegistry, } impl CursorActor { pub(crate) fn spawn( handle: CursorSessionHandle, mut receiver: mpsc::Receiver, dependencies: RunDependencies, blob_sync: BlobSynchronizer, next_append_seqno: i64, ) { tokio::spawn(async move { let mut inbox = OrderedInbox::starting_at(next_append_seqno); let (results_tx, results_rx) = tool_result_channel(); let (runtime_actions_tx, runtime_actions_rx) = mpsc::unbounded_channel(); let tool_runtime = CursorToolRuntime::default(); let context_sync = RequestContextSynchronizer::new(handle.clone(), dependencies.store.clone()); let tools = ToolDispatcher::with_results(tool_runtime.clone(), results_tx.clone()); let mut run_resources = Some((results_rx, runtime_actions_rx, dependencies)); loop { let command = match receiver.recv().await { Some(command) => command, None => { handle.cancel(); break; } }; match command { CursorCommand::Abort => { handle.cancel(); } CursorCommand::Finished => { break; } CursorCommand::Append { seqno, message } => { for (_seqno, message) in inbox.push(seqno, *message) { { match message.message { Some(pb::agent_client_message::Message::RunRequest( request, )) => { if let Some((results, runtime_actions, dependencies)) = run_resources.take() { let handle = handle.clone(); let blob_sync = blob_sync.clone(); let context_sync = context_sync.clone(); let tools = tools.clone(); let tool_runtime = tool_runtime.clone(); tokio::spawn(async move { let mut checkpoint = CheckpointBuilder::new( dependencies.store.clone(), blob_sync.clone(), handle .parent() .map(|parent| parent.tool_call_id.clone()), request.conversation_state.clone(), ); let parent = handle.parent().map(|parent| { ( crate::model::RunId::new(&parent.run_id), parent.tool_call_id.clone(), ) }); let prepared = request::prepare( handle.request_id(), &request, parent, request::PrepareDependencies { compiler: &dependencies.compiler, store: &dependencies.store, checkpoint: &checkpoint, blob_sync: &blob_sync, context_sync: &context_sync, }, ) .await; let (prepared, context) = match prepared { Ok(prepared) => prepared, Err(error) => { tracing::error!( request_id = handle.request_id(), %error, "failed to prepare Cursor Run" ); let _ = crate::cursor::lifecycle::fail( &handle, &error, ); let _ = handle .command(CursorCommand::Finished) .await; return; } }; checkpoint.configure( prepared.model.model_id.clone(), prepared.model.context_window_tokens, context.checkpoint_prompt.instructions.clone(), context.checkpoint_prompt.tools.clone(), context.dynamic_tools.keys().cloned().collect(), context.turn_user.clone(), ); let cancellation = handle.cancellation(); let (port, core) = crate::client::session(256); let actor = RunActor::new( dependencies.store.clone(), dependencies.provider, dependencies.run_registry, ); let core_run = actor.spawn(prepared, port, cancellation).await; let session = CursorSession::new( handle.clone(), dependencies.store, context, core, super::session::CursorSessionRuntime { tools, results, runtime_actions, compiler: dependencies.compiler, blob_sync, checkpoint, tool_runtime, }, ); if let Err(error) = session.run().await { tracing::error!( request_id = handle.request_id(), %error, "Cursor session failed" ); handle.cancel(); let _ = crate::cursor::lifecycle::fail( &handle, &error, ); } let _ = core_run.await; let _ = handle.command(CursorCommand::Finished).await; }); } else { let error = crate::Error::Protocol(format!( "duplicate RunRequest for request_id: {}", handle.request_id() )); tracing::error!( request_id = handle.request_id(), %error, "rejected duplicate Cursor RunRequest" ); results_tx.send_error(error); } } Some(pb::agent_client_message::Message::ExecClientMessage( message, )) => { if context_sync.handle_client(&message).await { continue; } match codec::client_event(&message, &tool_runtime).await { Ok(codec::ClientExecEvent::Delta(message)) => { let _ = handle.emit(&message); } Ok(codec::ClientExecEvent::Message(message)) => { let _ = handle.emit(&message); } Ok(codec::ClientExecEvent::Completed(result)) => { results_tx.send(*result) } Ok(codec::ClientExecEvent::Pending) => {} Err(error) => results_tx.send_error(error), } } Some( pb::agent_client_message::Message::ExecClientControlMessage( message, ), ) => { use pb::exec_client_control_message::Message; match message.message { Some(Message::StreamClose(close)) => { if context_sync.handle_stream_close(close.id).await { continue; } if tool_runtime.take_exec(close.id).await.is_some() { results_tx.send_error(crate::Error::Protocol(format!( "Exec stream closed before result for id: {}", close.id ))); } } Some(Message::Throw(throw)) => { if context_sync .handle_throw( throw.id, format!( "Cursor request context failed: {}", throw.error ), ) .await { continue; } match tool_runtime.take_exec(throw.id).await { Some(pending) => results_tx.send_error( crate::Error::Protocol(format!( "Exec {} failed: {}", pending.call.call_id, throw.error )), ), None => results_tx.send_error( crate::Error::Protocol(format!( "unknown ExecClientThrow id: {}", throw.id )), ), } } Some(Message::Heartbeat(_)) | None => {} } } Some( pb::agent_client_message::Message::InteractionResponse( message, ), ) => match tools.interaction_response(&message).await { Ok(ClientToolEvent::Completed(completion)) => { results_tx.send(*completion) } Ok(ClientToolEvent::Pending) => {} Err(error) => results_tx.send_error(error), }, Some(pb::agent_client_message::Message::KvClientMessage( message, )) => { let _ = blob_sync.handle_client(message).await; } // TODO: ConversationAction has two different delivery paths that // must not be conflated: // // 1. AgentRunRequest.action starts/resumes a Run. request::prepare // currently consumes UserMessageAction, // BackgroundTaskCompletionAction, SummarizeAction and // ExecutePlanAction. ResumeAction only works indirectly through // the absence of a new runtime event and still needs an explicit // implementation that consumes ResumeAction.request_context. // 2. AgentClientMessage::ConversationAction arrives while a Bidi Run // is already active and needs a runtime dispatcher here. Supporting // an Action in request::prepare does not mean this path supports it. // // Cursor 3.16 sends a queued follow-up as InjectContextAction. // It targets expected_run_id and asks the active Run to yield to the // queued message. The session owns this path because interruption must // abort active execs and publish a recoverable checkpoint before the // old Run ends. It must not be reduced to handle.cancel() here. // // The remaining unimplemented Action variants are // ShellCommandAction, StartPlanAction, // AsyncAskQuestionCompletionAction, CancelSubagentAction, // BackgroundShellAction, BackgroundSubagentAction, // SubscriptionNotificationAction and GoalContinuationAction. // CancelSubagentAction must not start an LLM; variants whose wire // behavior is not captured yet need evidence before assigning // semantics. Every unsupported runtime Action must return an explicit // Protocol Error rather than falling through silently. Some( pb::agent_client_message::Message::ConversationAction( action, ), ) => match action.action { Some( pb::conversation_action::Action::UserMessageAction(_), ) | Some(pb::conversation_action::Action::CancelAction(_)) => { handle.cancel(); } Some( pb::conversation_action::Action::InjectContextAction( action, ), ) => { if runtime_actions_tx.send(action).is_err() { results_tx.send_error(crate::Error::Protocol( "InjectContextAction arrived without an active Run" .into(), )); } } Some(action) => { results_tx.send_error(crate::Error::Protocol(format!( "unsupported runtime ConversationAction: {}", runtime_action_name(&action) ))); } None => results_tx.send_error(crate::Error::Protocol( "runtime ConversationAction has no action".into(), )), }, _ => {} } } } } } } }); } } fn runtime_action_name(action: &pb::conversation_action::Action) -> &'static str { use pb::conversation_action::Action; match action { Action::UserMessageAction(_) => "UserMessageAction", Action::ResumeAction(_) => "ResumeAction", Action::CancelAction(_) => "CancelAction", Action::SummarizeAction(_) => "SummarizeAction", Action::ShellCommandAction(_) => "ShellCommandAction", Action::StartPlanAction(_) => "StartPlanAction", Action::ExecutePlanAction(_) => "ExecutePlanAction", Action::AsyncAskQuestionCompletionAction(_) => "AsyncAskQuestionCompletionAction", Action::CancelSubagentAction(_) => "CancelSubagentAction", Action::BackgroundTaskCompletionAction(_) => "BackgroundTaskCompletionAction", Action::BackgroundShellAction(_) => "BackgroundShellAction", Action::BackgroundSubagentAction(_) => "BackgroundSubagentAction", Action::SubscriptionNotificationAction(_) => "SubscriptionNotificationAction", Action::GoalContinuationAction(_) => "GoalContinuationAction", Action::InjectContextAction(_) => "InjectContextAction", } }