Files
cursor-byok/server/src/cursor/actor.rs
T
leookun 71ddf71ec1 feat(api): enhance statistics storage management with scope options
- Introduced a new type `StatisticsStorageScope` to specify the scope for clearing statistics.
- Updated the `clearStatisticsStorage` API method to accept a scope parameter, allowing for selective clearing of detailed records or all statistics.
- Modified the demo API to handle the new scope parameter appropriately.
- Updated the SettingsPage component to include a selection for clearing scope, enhancing user control over statistics management.
- Added new translations for the updated messages related to statistics clearing in both English and Chinese.
2026-08-28 10:18:11 +08:00

453 lines
28 KiB
Rust

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<dyn Provider>,
pub compiler: PromptCompiler,
pub run_registry: RunRegistry,
}
impl CursorActor {
pub(crate) fn spawn(
handle: CursorSessionHandle,
mut receiver: mpsc::Receiver<CursorCommand>,
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(),
dependencies.store.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.mark_conversation_cancelled();
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(conversation_id) =
request.conversation_id.as_deref()
{
if let Err(error) =
handle.set_conversation_id(conversation_id)
{
tracing::error!(
request_id = handle.request_id(),
%error,
"invalid Cursor conversation id"
);
let _ =
crate::cursor::lifecycle::fail(&handle, &error);
let _ =
handle.command(CursorCommand::Finished).await;
return;
}
}
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 prepared = async {
let parent = match handle.parent() {
Some(parent) => {
let parent_run_id = dependencies
.store
.active_run_for_cursor_request(
&parent.request_id,
)
.await?
.ok_or_else(|| {
crate::Error::Protocol(format!(
"Cursor parent request {} has no active local Run",
parent.request_id
))
})?;
Some((
parent_run_id,
parent.tool_call_id.clone(),
))
}
None => None,
};
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
}
.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(),
);
if context.background_completion
&& dependencies
.run_registry
.insert_messages(
&prepared.conversation_id,
prepared.initial_messages.clone(),
)
.await
{
crate::cursor::lifecycle::finish_success(
&handle,
);
let _ = handle
.command(CursorCommand::Finished)
.await;
return;
}
let cancellation = handle.cancellation();
let (port, core) = crate::client::session(256);
let core_commands = core.commands.clone();
let actor = RunActor::new(
dependencies.store.clone(),
dependencies.provider,
dependencies.run_registry,
);
let core_run = actor
.spawn(
prepared,
port,
core_commands,
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"
);
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;
}
match codec::stream_closed(close.id, &tool_runtime)
.await
{
Ok(Some(completion)) => {
results_tx.send(completion)
}
Ok(None) => {}
Err(error) => results_tx.send_error(error),
}
}
Some(Message::Throw(throw)) => {
if context_sync
.handle_throw(
throw.id,
format!(
"Cursor request context failed: {}",
throw.error
),
)
.await
{
continue;
}
if tool_runtime.is_interrupted(throw.id).await {
tool_runtime.discard_exec(throw.id).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, BackgroundShellAction,
// BackgroundSubagentAction,
// SubscriptionNotificationAction and GoalContinuationAction.
// 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(_),
) => {
handle.mark_conversation_cancelled();
handle.cancel();
}
Some(pb::conversation_action::Action::CancelAction(_)) => {
handle.mark_conversation_cancelled();
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(
pb::conversation_action::Action::CancelSubagentAction(
action,
),
) => {
if let Some(id) = tool_runtime
.running_task_exec_id(&action.subagent_id)
.await
{
let _ = handle.emit(&codec::abort(id));
}
}
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",
}
}