mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 12:13:05 +08:00
714 lines
33 KiB
Rust
714 lines
33 KiB
Rust
use std::collections::{BTreeMap, HashMap, HashSet, VecDeque};
|
|
|
|
use tokio::sync::{mpsc, oneshot};
|
|
|
|
use crate::{
|
|
client::{ClientCommand, ClientEvent, ClientSession, CommitCause},
|
|
cursor::{
|
|
blob_sync::BlobSynchronizer,
|
|
checkpoint::{
|
|
worker::{CheckpointJob, CheckpointKind, CheckpointWorker, FinalCheckpoints},
|
|
CheckpointBuilder,
|
|
},
|
|
interaction,
|
|
presentation::Presentation,
|
|
prompting::PromptCompiler,
|
|
proto::agent::v1 as pb,
|
|
request::CursorRunContext,
|
|
tools::{
|
|
codec,
|
|
result::{ToolCompletion, ToolResultReceiver},
|
|
runtime::CursorToolRuntime,
|
|
stream::ToolCallStream,
|
|
ToolBatchState, ToolDispatcher,
|
|
},
|
|
},
|
|
model::{ToolCall, ToolRoundId, Usage},
|
|
run::{RunFailure, RunOutcome},
|
|
store::{Store, ToolRoundStatus},
|
|
Error, Result,
|
|
};
|
|
|
|
use super::CursorSessionHandle;
|
|
|
|
pub struct CursorSession {
|
|
handle: CursorSessionHandle,
|
|
store: Store,
|
|
context: CursorRunContext,
|
|
core: ClientSession,
|
|
tools: ToolDispatcher,
|
|
results: ToolResultReceiver,
|
|
checkpoint: CheckpointBuilder,
|
|
tool_runtime: CursorToolRuntime,
|
|
runtime_actions: mpsc::UnboundedReceiver<pb::InjectContextAction>,
|
|
compiler: PromptCompiler,
|
|
blob_sync: BlobSynchronizer,
|
|
injection_ids: HashSet<String>,
|
|
pending_injections: HashMap<String, PendingInjection>,
|
|
}
|
|
|
|
struct PendingInjection {
|
|
user_message: Option<pb::UserMessage>,
|
|
delivery_batch_id: String,
|
|
}
|
|
|
|
pub(crate) struct CursorSessionRuntime {
|
|
pub tools: ToolDispatcher,
|
|
pub results: ToolResultReceiver,
|
|
pub checkpoint: CheckpointBuilder,
|
|
pub tool_runtime: CursorToolRuntime,
|
|
pub runtime_actions: mpsc::UnboundedReceiver<pb::InjectContextAction>,
|
|
pub compiler: PromptCompiler,
|
|
pub blob_sync: BlobSynchronizer,
|
|
}
|
|
|
|
impl CursorSession {
|
|
pub(crate) fn new(
|
|
handle: CursorSessionHandle,
|
|
store: Store,
|
|
context: CursorRunContext,
|
|
core: ClientSession,
|
|
runtime: CursorSessionRuntime,
|
|
) -> Self {
|
|
Self {
|
|
handle,
|
|
store,
|
|
context,
|
|
core,
|
|
tools: runtime.tools,
|
|
results: runtime.results,
|
|
checkpoint: runtime.checkpoint,
|
|
tool_runtime: runtime.tool_runtime,
|
|
runtime_actions: runtime.runtime_actions,
|
|
compiler: runtime.compiler,
|
|
blob_sync: runtime.blob_sync,
|
|
injection_ids: HashSet::new(),
|
|
pending_injections: HashMap::new(),
|
|
}
|
|
}
|
|
|
|
pub async fn run(mut self) -> Result<()> {
|
|
if self.context.compacting {
|
|
self.handle.emit(&interaction::summary_started())?;
|
|
}
|
|
let mut worker = CheckpointWorker::spawn(
|
|
self.store.clone(),
|
|
self.checkpoint.clone(),
|
|
self.handle.clone(),
|
|
self.context.mode,
|
|
);
|
|
let mut checkpoint_worker_open = true;
|
|
let mut calls = BTreeMap::<usize, ToolCall>::new();
|
|
let mut streams = BTreeMap::<usize, ToolCallStream>::new();
|
|
let mut completions = HashMap::<String, ToolCompletion>::new();
|
|
let mut completed = HashSet::<String>::new();
|
|
let mut response_text = String::new();
|
|
let mut response_thinking = String::new();
|
|
let mut active_round = None::<ToolRoundId>;
|
|
let mut final_checkpoint = None::<FinalCheckpoints>;
|
|
let mut compaction_checkpoint = None::<pb::ConversationStateStructure>;
|
|
let mut turn_usage = None::<Usage>;
|
|
let mut context_tokens = None::<u64>;
|
|
let mut ready = VecDeque::new();
|
|
let mut presentation = Presentation::default();
|
|
|
|
loop {
|
|
let input = if let Some(completion) = ready.pop_front() {
|
|
Input::Completion(completion)
|
|
} else {
|
|
tokio::select! {
|
|
event = self.core.events.recv() => Input::Event(event),
|
|
completion = self.results.recv() => Input::CompletionResult(completion),
|
|
action = self.runtime_actions.recv() => Input::RuntimeAction(action.map(Box::new)),
|
|
failure = worker.failures.recv(), if checkpoint_worker_open => Input::CheckpointFailure(failure),
|
|
}
|
|
};
|
|
match input {
|
|
Input::CheckpointFailure(Some(error)) => return Err(error),
|
|
Input::CheckpointFailure(None) => {
|
|
checkpoint_worker_open = false;
|
|
}
|
|
Input::Completion(completion) => {
|
|
if let Some(completion) = self
|
|
.forward_completion(completion, &mut completions)
|
|
.await?
|
|
{
|
|
ready.push_back(completion);
|
|
}
|
|
}
|
|
Input::CompletionResult(Some(result)) => {
|
|
if let Some(completion) =
|
|
self.forward_completion(result?, &mut completions).await?
|
|
{
|
|
ready.push_back(completion);
|
|
}
|
|
}
|
|
Input::CompletionResult(None) => {
|
|
return Err(Error::Protocol("tool result channel closed".into()));
|
|
}
|
|
Input::RuntimeAction(Some(action)) => {
|
|
self.forward_injection(*action).await?;
|
|
}
|
|
Input::RuntimeAction(None) => {
|
|
return Err(Error::Protocol("runtime action channel closed".into()));
|
|
}
|
|
Input::Event(None) => {
|
|
worker.abort();
|
|
return Err(Error::Protocol("core event channel closed".into()));
|
|
}
|
|
Input::Event(Some(event)) => match event {
|
|
ClientEvent::AutoCompactionStarted => {
|
|
self.handle.emit(&interaction::summary_started())?;
|
|
}
|
|
ClientEvent::AutoCompactionCompleted => {
|
|
self.handle.emit(&interaction::summary_completed())?;
|
|
}
|
|
ClientEvent::TextStart => {}
|
|
ClientEvent::TextEnd => {
|
|
if !self.context.compacting {
|
|
presentation.finish_text();
|
|
}
|
|
}
|
|
ClientEvent::TextDelta(delta) => {
|
|
response_text.push_str(&delta);
|
|
if self.context.compacting {
|
|
self.handle.emit(&interaction::summary_delta(delta))?;
|
|
} else {
|
|
presentation.text_delta(&delta);
|
|
self.emit_model_event(
|
|
crate::provider::ModelEvent::TextDelta(delta),
|
|
"",
|
|
)?;
|
|
}
|
|
}
|
|
ClientEvent::ThinkingStart => {}
|
|
ClientEvent::ThinkingDelta(delta) => {
|
|
response_thinking.push_str(&delta);
|
|
if !self.context.compacting {
|
|
presentation.thinking_delta(&delta);
|
|
self.emit_model_event(
|
|
crate::provider::ModelEvent::ThinkingDelta(delta),
|
|
"",
|
|
)?;
|
|
}
|
|
}
|
|
ClientEvent::ThinkingEnd { duration } => {
|
|
if !self.context.compacting {
|
|
presentation.finish_thinking(duration);
|
|
self.handle
|
|
.emit(&interaction::thinking_completed(duration))?;
|
|
}
|
|
}
|
|
ClientEvent::ToolCallStart {
|
|
index,
|
|
call_id,
|
|
name,
|
|
model_call_id,
|
|
} => {
|
|
let call = ToolCall {
|
|
index,
|
|
call_id: call_id.clone(),
|
|
model_call_id: model_call_id.clone(),
|
|
name: name.clone(),
|
|
arguments_text: String::new(),
|
|
arguments: serde_json::Value::Null,
|
|
};
|
|
self.emit_model_event(
|
|
crate::provider::ModelEvent::ToolCallStart {
|
|
index,
|
|
call_id,
|
|
name: name.clone(),
|
|
},
|
|
&model_call_id,
|
|
)?;
|
|
streams.insert(
|
|
index,
|
|
ToolCallStream::new(&name, self.context.dynamic_tools.get(&name)),
|
|
);
|
|
calls.insert(index, call);
|
|
}
|
|
ClientEvent::ToolCallArgumentsDelta { index, delta } => {
|
|
let call = calls.get_mut(&index).ok_or_else(|| {
|
|
Error::Protocol(format!("unknown streaming tool index: {index}"))
|
|
})?;
|
|
call.arguments_text.push_str(&delta);
|
|
let stream = streams.get_mut(&index).ok_or_else(|| {
|
|
Error::Protocol(format!("missing Cursor tool stream: {index}"))
|
|
})?;
|
|
for message in stream.arguments_delta(call, &delta)? {
|
|
self.handle.emit(&message)?;
|
|
}
|
|
}
|
|
ClientEvent::ToolCallEnd { index } => {
|
|
let call = calls.get_mut(&index).ok_or_else(|| {
|
|
Error::Protocol(format!("unknown completed tool index: {index}"))
|
|
})?;
|
|
call.arguments = serde_json::from_str(&call.arguments_text)?;
|
|
}
|
|
ClientEvent::Usage(usage) => {
|
|
if !self.context.compacting {
|
|
if let Some(output_tokens) = usage.output_tokens {
|
|
self.handle.emit(&interaction::token_delta(output_tokens))?;
|
|
}
|
|
}
|
|
if !self.context.compacting {
|
|
context_tokens = usage
|
|
.input_tokens
|
|
.zip(usage.output_tokens)
|
|
.and_then(|(input, output)| input.checked_add(output));
|
|
}
|
|
match &mut turn_usage {
|
|
Some(total) => *total += usage,
|
|
None => turn_usage = Some(usage),
|
|
}
|
|
}
|
|
ClientEvent::ExecuteToolRound {
|
|
round_id,
|
|
calls: round_calls,
|
|
} => {
|
|
active_round = Some(round_id);
|
|
for dispatched in self
|
|
.tools
|
|
.start_batch(
|
|
&round_calls,
|
|
ToolBatchState {
|
|
completed: &completed,
|
|
started: &HashSet::new(),
|
|
response_text: &response_text,
|
|
response_thinking: &response_thinking,
|
|
},
|
|
&self
|
|
.store
|
|
.load_current_messages(&crate::model::ConversationId::new(
|
|
&self.context.exec.conversation_id,
|
|
))
|
|
.await?,
|
|
&self.context.dynamic_tools,
|
|
&self.context.exec,
|
|
)
|
|
.await?
|
|
{
|
|
for message in dispatched.messages {
|
|
self.handle.emit(&message)?;
|
|
}
|
|
if let Some(completion) = dispatched.completion {
|
|
ready.push_back(completion);
|
|
}
|
|
}
|
|
response_text.clear();
|
|
response_thinking.clear();
|
|
calls.clear();
|
|
streams.clear();
|
|
}
|
|
ClientEvent::StateCommitted(state) => {
|
|
if matches!(&state.cause, CommitCause::RuntimeEvent { .. }) {
|
|
response_text.clear();
|
|
response_thinking.clear();
|
|
calls.clear();
|
|
streams.clear();
|
|
}
|
|
if let CommitCause::RuntimeEvent { event_id } = &state.cause {
|
|
if let Some(injection_id) = event_id.strip_prefix("inject-context:") {
|
|
if let Some(pending) = self.pending_injections.remove(injection_id)
|
|
{
|
|
let delivered_at_ms = crate::cursor::tools::runtime::now_ms()
|
|
.min(i64::MAX as u64)
|
|
as i64;
|
|
self.handle.emit(&interaction::context_injection_delivered(
|
|
injection_id.to_owned(),
|
|
pending.delivery_batch_id.clone(),
|
|
delivered_at_ms,
|
|
))?;
|
|
if let Some(user_message) = pending.user_message {
|
|
self.handle.emit(&interaction::user_message_appended(
|
|
user_message,
|
|
))?;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let CommitCause::ToolRoundStarted(round_id) = &state.cause {
|
|
active_round = Some(round_id.clone());
|
|
}
|
|
let mut tool_round_settled = false;
|
|
if let CommitCause::ToolResult { call_id } = &state.cause {
|
|
let completion = completions.remove(call_id).ok_or_else(|| {
|
|
Error::Protocol(format!(
|
|
"core committed a tool result without typed Cursor state: {call_id}"
|
|
))
|
|
})?;
|
|
let snapshot = self
|
|
.store
|
|
.tool_round(active_round.as_ref().ok_or_else(|| {
|
|
Error::Protocol("tool commit has no active round".into())
|
|
})?)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
Error::Store("active tool round disappeared".into())
|
|
})?;
|
|
let call = snapshot
|
|
.calls
|
|
.iter()
|
|
.find(|call| call.call_id == *call_id)
|
|
.ok_or_else(|| {
|
|
Error::Protocol(format!(
|
|
"committed call is absent from tool round: {call_id}"
|
|
))
|
|
})?;
|
|
self.handle
|
|
.emit(&interaction::tool_completed(call, &completion))?;
|
|
presentation.tool_completed(&completion);
|
|
completed.insert(call_id.clone());
|
|
tool_round_settled = snapshot.status == ToolRoundStatus::Settled;
|
|
}
|
|
let final_turn = state.cause == CommitCause::FinalTurn;
|
|
if let CommitCause::Compaction { summary } = &state.cause {
|
|
if !state.barrier.is_required() {
|
|
return Err(Error::Protocol(
|
|
"compaction state has no completion barrier".into(),
|
|
));
|
|
}
|
|
let (sender, receiver) = oneshot::channel();
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::Compaction {
|
|
revision_id: state.revision_id,
|
|
summary: summary.clone(),
|
|
result: sender,
|
|
},
|
|
presentation: presentation.take(),
|
|
context_tokens: None,
|
|
ready: None,
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
match receiver
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker stopped".into()))?
|
|
{
|
|
Ok(checkpoint) => {
|
|
compaction_checkpoint = Some(checkpoint);
|
|
state.barrier.complete(Ok(()));
|
|
}
|
|
Err(error) => {
|
|
state.barrier.complete(Err(error.to_string()));
|
|
return Err(error);
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if final_turn {
|
|
if !state.barrier.is_required() {
|
|
return Err(Error::Protocol(
|
|
"final state has no completion barrier".into(),
|
|
));
|
|
}
|
|
let (sender, receiver) = oneshot::channel();
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::Final {
|
|
revision_id: state.revision_id,
|
|
result: sender,
|
|
},
|
|
presentation: presentation.take(),
|
|
context_tokens,
|
|
ready: None,
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
match receiver
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker stopped".into()))?
|
|
{
|
|
Ok(checkpoints) => {
|
|
final_checkpoint = Some(checkpoints);
|
|
state.barrier.complete(Ok(()));
|
|
}
|
|
Err(error) => {
|
|
state.barrier.complete(Err(error.to_string()));
|
|
return Err(error);
|
|
}
|
|
}
|
|
} else if let CommitCause::ToolRoundStarted(round_id) = &state.cause {
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::ToolStarted {
|
|
round_id: round_id.clone(),
|
|
stable_revision_id: state.revision_id,
|
|
},
|
|
presentation: presentation.take(),
|
|
context_tokens,
|
|
ready: None,
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
} else if tool_round_settled {
|
|
if !state.barrier.is_required() {
|
|
return Err(Error::Protocol(
|
|
"settled tool round has no completion barrier".into(),
|
|
));
|
|
}
|
|
let (ready, published) = oneshot::channel();
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::ToolSettled(state.revision_id),
|
|
presentation: presentation.take(),
|
|
context_tokens,
|
|
ready: Some(ready),
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
let result = published
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker stopped".into()))?
|
|
.map_err(Error::Protocol);
|
|
match result {
|
|
Ok(()) => state.barrier.complete(Ok(())),
|
|
Err(error) => {
|
|
state.barrier.complete(Err(error.to_string()));
|
|
return Err(error);
|
|
}
|
|
}
|
|
active_round = None;
|
|
self.tool_runtime.clear_completed().await;
|
|
} else if !matches!(&state.cause, CommitCause::ToolResult { .. })
|
|
&& active_round.is_some()
|
|
{
|
|
let round_id = active_round.clone().ok_or_else(|| {
|
|
Error::Protocol("active tool round disappeared".into())
|
|
})?;
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::ToolStarted {
|
|
round_id,
|
|
stable_revision_id: state.revision_id,
|
|
},
|
|
presentation: presentation.take(),
|
|
context_tokens,
|
|
ready: None,
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
} else if !matches!(&state.cause, CommitCause::ToolResult { .. }) {
|
|
let requires_ready = state.barrier.is_required();
|
|
let (ready, published) = oneshot::channel();
|
|
worker
|
|
.jobs
|
|
.send(CheckpointJob {
|
|
kind: CheckpointKind::Settled(state.revision_id),
|
|
presentation: presentation.take(),
|
|
context_tokens,
|
|
ready: requires_ready.then_some(ready),
|
|
})
|
|
.await
|
|
.map_err(|_| Error::Protocol("checkpoint worker closed".into()))?;
|
|
if requires_ready {
|
|
let result = published
|
|
.await
|
|
.map_err(|_| {
|
|
Error::Protocol("checkpoint worker stopped".into())
|
|
})?
|
|
.map_err(Error::Protocol);
|
|
match result {
|
|
Ok(()) => state.barrier.complete(Ok(())),
|
|
Err(error) => {
|
|
state.barrier.complete(Err(error.to_string()));
|
|
return Err(error);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
ClientEvent::Ended(outcome) => {
|
|
return match outcome {
|
|
RunOutcome::Completed => {
|
|
if self.context.compacting {
|
|
let checkpoint =
|
|
compaction_checkpoint.take().ok_or_else(|| {
|
|
Error::Protocol(
|
|
"Completed compaction without checkpoint".into(),
|
|
)
|
|
})?;
|
|
self.handle.emit(&interaction::summary_completed())?;
|
|
self.handle.emit(&interaction::turn_ended(turn_usage))?;
|
|
for _ in 0..3 {
|
|
self.checkpoint.publish(&self.handle, &checkpoint).await?;
|
|
}
|
|
crate::cursor::lifecycle::finish_success(&self.handle);
|
|
return Ok(());
|
|
}
|
|
let checkpoints = final_checkpoint.take().ok_or_else(|| {
|
|
Error::Protocol("Completed without final state".into())
|
|
})?;
|
|
self.handle.emit(&interaction::turn_ended(turn_usage))?;
|
|
self.checkpoint
|
|
.publish(&self.handle, &checkpoints.staged)
|
|
.await?;
|
|
self.checkpoint
|
|
.publish(&self.handle, &checkpoints.settled)
|
|
.await?;
|
|
self.handle.emit(&pb::AgentServerMessage {
|
|
ttft_breakdown: None,
|
|
message: Some(pb::agent_server_message::Message::ConversationCheckpointUpdate(checkpoints.settled)),
|
|
})?;
|
|
crate::cursor::lifecycle::finish_success(&self.handle);
|
|
Ok(())
|
|
}
|
|
RunOutcome::Cancelled => {
|
|
worker.abort();
|
|
self.abort_execs().await;
|
|
crate::cursor::lifecycle::cancel(&self.handle)
|
|
}
|
|
RunOutcome::Failed(failure) => {
|
|
worker.abort();
|
|
self.abort_execs().await;
|
|
crate::cursor::lifecycle::fail(&self.handle, &cursor_error(failure))
|
|
}
|
|
};
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn abort_execs(&self) {
|
|
for id in self.tool_runtime.drain_running().await {
|
|
let _ = self.handle.emit(&codec::abort(id));
|
|
}
|
|
}
|
|
|
|
async fn forward_completion(
|
|
&self,
|
|
mut completion: ToolCompletion,
|
|
completions: &mut HashMap<String, ToolCompletion>,
|
|
) -> Result<Option<ToolCompletion>> {
|
|
if let Some(image) = completion.take_read_image() {
|
|
let blob_id = self.store.put_blob(&image.data, &[]).await?;
|
|
completion.persist_read_image(&blob_id, &image)?;
|
|
}
|
|
let result = completion.result();
|
|
if result.call_id.is_empty() {
|
|
return Err(Error::Protocol("tool result call_id is empty".into()));
|
|
}
|
|
if completions
|
|
.insert(result.call_id.clone(), completion.clone())
|
|
.is_some()
|
|
{
|
|
return Err(Error::Protocol(format!(
|
|
"duplicate tool result call_id: {}",
|
|
result.call_id
|
|
)));
|
|
}
|
|
self.core
|
|
.commands
|
|
.send(ClientCommand::ToolResult(result.clone()))
|
|
.await
|
|
.map_err(|_| Error::RunNotFound(self.context.request_id.clone()))?;
|
|
let Some(dispatched) = self.tools.continue_after(&result.call_id).await? else {
|
|
return Ok(None);
|
|
};
|
|
for message in dispatched.messages {
|
|
self.handle.emit(&message)?;
|
|
}
|
|
Ok(dispatched.completion)
|
|
}
|
|
|
|
async fn forward_injection(&mut self, action: pb::InjectContextAction) -> Result<()> {
|
|
if action.injection_id.is_empty() {
|
|
return Err(Error::Protocol(
|
|
"InjectContextAction has no injection_id".into(),
|
|
));
|
|
}
|
|
if action.expected_run_id != self.context.request_id {
|
|
return Err(Error::Protocol(format!(
|
|
"InjectContextAction expected run {}, active run is {}",
|
|
action.expected_run_id, self.context.request_id
|
|
)));
|
|
}
|
|
if self.injection_ids.contains(&action.injection_id) {
|
|
return Ok(());
|
|
}
|
|
let user_message = match action.payload.as_ref() {
|
|
Some(pb::inject_context_action::Payload::UserContext(context)) => {
|
|
context.user_message.clone()
|
|
}
|
|
_ => None,
|
|
};
|
|
let message = crate::cursor::request::compile_injection(
|
|
&action,
|
|
self.context.mode,
|
|
&self.compiler,
|
|
&self.blob_sync,
|
|
)
|
|
.await?;
|
|
let injection_id = action.injection_id;
|
|
let delivery_batch_id = injection_id.clone();
|
|
self.injection_ids.insert(injection_id.clone());
|
|
self.pending_injections.insert(
|
|
injection_id.clone(),
|
|
PendingInjection {
|
|
user_message,
|
|
delivery_batch_id,
|
|
},
|
|
);
|
|
self.handle
|
|
.emit(&interaction::context_injection_queued(injection_id.clone()))?;
|
|
if self
|
|
.core
|
|
.commands
|
|
.send(ClientCommand::RuntimeMessage(message))
|
|
.await
|
|
.is_err()
|
|
{
|
|
self.pending_injections.remove(&injection_id);
|
|
return Err(Error::RunNotFound(self.context.request_id.clone()));
|
|
}
|
|
self.interrupt_execs().await;
|
|
Ok(())
|
|
}
|
|
|
|
async fn interrupt_execs(&self) {
|
|
// Keep runtime entries until Cursor returns the aborted result. The core tool
|
|
// round needs that terminal result before it can append the injected context
|
|
// after the complete assistant/tool pair and continue the same Run.
|
|
for id in self.tool_runtime.running_exec_ids().await {
|
|
let _ = self.handle.emit(&codec::abort(id));
|
|
}
|
|
}
|
|
|
|
fn emit_model_event(
|
|
&self,
|
|
event: crate::provider::ModelEvent,
|
|
model_call_id: &str,
|
|
) -> Result<()> {
|
|
if let Some(message) =
|
|
interaction::response_event(&event, model_call_id, &self.context.dynamic_tools)?
|
|
{
|
|
self.handle.emit(&message)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
enum Input {
|
|
Event(Option<ClientEvent>),
|
|
Completion(ToolCompletion),
|
|
CompletionResult(Option<Result<ToolCompletion>>),
|
|
RuntimeAction(Option<Box<pb::InjectContextAction>>),
|
|
CheckpointFailure(Option<Error>),
|
|
}
|
|
|
|
fn cursor_error(failure: RunFailure) -> Error {
|
|
match failure {
|
|
RunFailure::Protocol(message) => Error::Protocol(message),
|
|
RunFailure::Provider(message) => Error::Provider(message),
|
|
RunFailure::Store(message) => Error::Store(message),
|
|
RunFailure::Client(message) => Error::Protocol(message),
|
|
}
|
|
}
|