mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-05 03:56:45 +08:00
stabilize cursor request context and release workflow
This commit is contained in:
@@ -30,17 +30,15 @@ pub fn decode(data: &[u8], internal_id: String) -> Result<CanonicalMessage> {
|
||||
.and_then(Value::as_str)
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
let is_request_context = role == Role::User && wire_id.starts_with("request-context:");
|
||||
let is_prompt_context =
|
||||
is_request_context || role == Role::User && wire_id.starts_with("selected-context:");
|
||||
let origin = match role {
|
||||
Role::System => Origin::Prompt,
|
||||
Role::Assistant => Origin::Assistant,
|
||||
Role::Tool => Origin::Tool,
|
||||
Role::User if wire_id.starts_with("runtime:") => Origin::Runtime,
|
||||
Role::User
|
||||
if wire_id.starts_with("request-context:")
|
||||
|| wire_id.starts_with("selected-context:") =>
|
||||
{
|
||||
Origin::Prompt
|
||||
}
|
||||
Role::User if is_prompt_context => Origin::Prompt,
|
||||
Role::User => Origin::User,
|
||||
};
|
||||
let runtime_event_id = wire_id.strip_prefix("runtime:").map(str::to_string);
|
||||
@@ -49,7 +47,7 @@ pub fn decode(data: &[u8], internal_id: String) -> Result<CanonicalMessage> {
|
||||
Role::Tool => MessageContent::ToolResult(decode_tool_result(&value)?),
|
||||
_ => decode_text(&value)?,
|
||||
};
|
||||
let message_id = if runtime_event_id.is_some() {
|
||||
let message_id = if runtime_event_id.is_some() || is_request_context {
|
||||
wire_id
|
||||
} else {
|
||||
internal_id
|
||||
|
||||
@@ -131,6 +131,23 @@ fn runtime_wire_identity_survives_checkpoint_hydration() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn request_context_identity_survives_checkpoint_hydration() {
|
||||
let wire = json!({
|
||||
"role": "user",
|
||||
"id": "request-context:digest",
|
||||
"content": "<rules>current rules</rules>",
|
||||
});
|
||||
let message = decode(
|
||||
serde_json::to_vec(&wire).unwrap().as_slice(),
|
||||
"cursor-root:blob-id:20".into(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(message.message_id, "request-context:digest");
|
||||
assert_eq!(message.origin, crate::model::Origin::Prompt);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cursor_user_image_uses_image_field() {
|
||||
let wire = json!({
|
||||
|
||||
@@ -134,7 +134,6 @@ impl PromptAssets {
|
||||
}
|
||||
|
||||
const RUNTIME_VARIABLES: &[&str] = &[
|
||||
"REQUEST_CONTEXT",
|
||||
"OPEN_FILES",
|
||||
"SELECTED_CONTEXT",
|
||||
"ACTION_CONTEXT",
|
||||
|
||||
@@ -192,6 +192,20 @@ pub(crate) async fn prepare(
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let request_context_message = match event_id.as_deref() {
|
||||
Some(event_id) if !compacting && !background_completion => {
|
||||
let message_id = format!("request-context:{event_id}");
|
||||
match store.message(&conversation_id, &message_id).await? {
|
||||
Some(message) => Some(message),
|
||||
None => runtime::compile_request_context(
|
||||
event_id,
|
||||
&request_context,
|
||||
base_messages.as_deref().unwrap_or_default(),
|
||||
)?,
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
let initial_messages = if compacting {
|
||||
Vec::new()
|
||||
} else {
|
||||
@@ -209,21 +223,27 @@ pub(crate) async fn prepare(
|
||||
turn_user = Some(user);
|
||||
vec![message]
|
||||
}
|
||||
(Some(user), Some(event_id)) => match existing_runtime {
|
||||
Some(message) => vec![message],
|
||||
None => vec![
|
||||
runtime::compile(
|
||||
event_id,
|
||||
checkpoint_mode,
|
||||
&user,
|
||||
&request_context,
|
||||
&action_context,
|
||||
compiler,
|
||||
blob_sync,
|
||||
)
|
||||
.await?,
|
||||
],
|
||||
},
|
||||
(Some(user), Some(event_id)) => {
|
||||
let runtime = match existing_runtime {
|
||||
Some(message) => message,
|
||||
None => {
|
||||
runtime::compile(
|
||||
event_id,
|
||||
checkpoint_mode,
|
||||
&user,
|
||||
&request_context,
|
||||
&action_context,
|
||||
compiler,
|
||||
blob_sync,
|
||||
)
|
||||
.await?
|
||||
}
|
||||
};
|
||||
request_context_message
|
||||
.into_iter()
|
||||
.chain(std::iter::once(runtime))
|
||||
.collect()
|
||||
}
|
||||
(None, None) => Vec::new(),
|
||||
_ => {
|
||||
return Err(Error::Protocol(
|
||||
|
||||
@@ -87,10 +87,6 @@ pub async fn compile(
|
||||
.map(|env| env.time_zone.as_str()),
|
||||
)?;
|
||||
let mut values = BTreeMap::from([
|
||||
(
|
||||
"REQUEST_CONTEXT",
|
||||
section(context::compile_context(request_context, &time.today)),
|
||||
),
|
||||
("OPEN_FILES", section(open_files(user))),
|
||||
(
|
||||
"SELECTED_CONTEXT",
|
||||
@@ -122,6 +118,41 @@ pub async fn compile(
|
||||
.await
|
||||
}
|
||||
|
||||
pub(super) fn compile_request_context(
|
||||
event_id: &str,
|
||||
request_context: &pb::RequestContext,
|
||||
history: &[CanonicalMessage],
|
||||
) -> Result<Option<CanonicalMessage>> {
|
||||
let time = Time::now(
|
||||
request_context
|
||||
.env
|
||||
.as_ref()
|
||||
.map(|env| env.time_zone.as_str()),
|
||||
)?;
|
||||
let text = context::compile_context(request_context, &time.today);
|
||||
if text.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
let message = CanonicalMessage::text(
|
||||
format!("request-context:{event_id}"),
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
text,
|
||||
);
|
||||
Ok(should_project_request_context(history, &message).then_some(message))
|
||||
}
|
||||
|
||||
fn should_project_request_context(
|
||||
history: &[CanonicalMessage],
|
||||
current: &CanonicalMessage,
|
||||
) -> bool {
|
||||
history
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|message| message.message_id.starts_with("request-context:"))
|
||||
.is_none_or(|previous| previous.content != current.content)
|
||||
}
|
||||
|
||||
pub async fn compile_background(
|
||||
event_id: String,
|
||||
user: &pb::UserMessage,
|
||||
@@ -252,3 +283,55 @@ impl Time {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn request_context_is_only_projected_when_its_content_changes() {
|
||||
let first = CanonicalMessage::text(
|
||||
"request-context:first",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"<rules>same</rules>",
|
||||
);
|
||||
let duplicate = CanonicalMessage::text(
|
||||
"request-context:second",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"<rules>same</rules>",
|
||||
);
|
||||
let changed = CanonicalMessage::text(
|
||||
"request-context:third",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"<rules>changed</rules>",
|
||||
);
|
||||
let runtime = CanonicalMessage::text(
|
||||
"runtime:turn",
|
||||
Role::User,
|
||||
Origin::Runtime,
|
||||
"<user_query>next</user_query>",
|
||||
);
|
||||
|
||||
assert!(should_project_request_context(&[], &first));
|
||||
assert!(!should_project_request_context(
|
||||
&[first.clone(), runtime.clone()],
|
||||
&duplicate
|
||||
));
|
||||
assert!(should_project_request_context(
|
||||
&[first.clone(), runtime.clone()],
|
||||
&changed
|
||||
));
|
||||
assert!(should_project_request_context(
|
||||
&[first.clone(), changed, runtime],
|
||||
&CanonicalMessage::text(
|
||||
"request-context:fourth",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"<rules>same</rules>",
|
||||
)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -460,11 +460,8 @@ impl RunEngine {
|
||||
.iter()
|
||||
.map(|message| message.message_id.as_str())
|
||||
.collect::<HashSet<_>>();
|
||||
let compactable = messages
|
||||
.iter()
|
||||
.filter(|message| !current_ids.contains(message.message_id.as_str()))
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let (compactable, retained_request_context) =
|
||||
auto_compaction_partition(messages, ¤t_ids);
|
||||
if compactable.is_empty() {
|
||||
return Ok((revision, None));
|
||||
}
|
||||
@@ -533,7 +530,8 @@ impl RunEngine {
|
||||
},
|
||||
runtime_event_id: Some(event_id),
|
||||
};
|
||||
let mut replacement = vec![summary_message];
|
||||
let mut replacement = retained_request_context.into_iter().collect::<Vec<_>>();
|
||||
replacement.push(summary_message);
|
||||
replacement.extend(prepared.initial_messages.iter().cloned());
|
||||
let revision = self
|
||||
.store
|
||||
@@ -565,6 +563,29 @@ impl RunEngine {
|
||||
}
|
||||
}
|
||||
|
||||
fn auto_compaction_partition(
|
||||
messages: &[CanonicalMessage],
|
||||
current_ids: &HashSet<&str>,
|
||||
) -> (Vec<CanonicalMessage>, Option<CanonicalMessage>) {
|
||||
let latest_request_context = messages
|
||||
.iter()
|
||||
.rposition(|message| message.message_id.starts_with("request-context:"));
|
||||
let compactable = messages
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, message)| {
|
||||
Some(*index) != latest_request_context
|
||||
&& !current_ids.contains(message.message_id.as_str())
|
||||
})
|
||||
.map(|(_, message)| message.clone())
|
||||
.collect();
|
||||
let retained = latest_request_context
|
||||
.and_then(|index| messages.get(index))
|
||||
.filter(|message| !current_ids.contains(message.message_id.as_str()))
|
||||
.cloned();
|
||||
(compactable, retained)
|
||||
}
|
||||
|
||||
fn should_auto_compact(prepared: &PreparedRun, messages: &[CanonicalMessage]) -> bool {
|
||||
if prepared.action != RunAction::Start {
|
||||
return false;
|
||||
@@ -724,7 +745,7 @@ fn failure_message(failure: &RunFailure) -> String {
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{estimate_context_tokens, hydrate_tool_images};
|
||||
use super::{auto_compaction_partition, estimate_context_tokens, hydrate_tool_images};
|
||||
use crate::{
|
||||
model::{
|
||||
CanonicalMessage, ContentPart, Origin, ProjectedContent, ProjectedMessage, PromptSpec,
|
||||
@@ -732,6 +753,7 @@ mod tests {
|
||||
},
|
||||
store::Store,
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
|
||||
#[test]
|
||||
fn context_estimate_grows_with_prompt_history() {
|
||||
@@ -756,6 +778,42 @@ mod tests {
|
||||
assert!(estimate_context_tokens(&prompt, &long) > estimate_context_tokens(&prompt, &short));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_compaction_preserves_only_the_latest_request_context() {
|
||||
let first_context = CanonicalMessage::text(
|
||||
"request-context:first",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"old rules",
|
||||
);
|
||||
let old_runtime =
|
||||
CanonicalMessage::text("runtime:first", Role::User, Origin::Runtime, "old query");
|
||||
let latest_context = CanonicalMessage::text(
|
||||
"request-context:second",
|
||||
Role::User,
|
||||
Origin::Prompt,
|
||||
"new rules",
|
||||
);
|
||||
let current_runtime = CanonicalMessage::text(
|
||||
"runtime:current",
|
||||
Role::User,
|
||||
Origin::Runtime,
|
||||
"current query",
|
||||
);
|
||||
let messages = vec![
|
||||
first_context.clone(),
|
||||
old_runtime.clone(),
|
||||
latest_context.clone(),
|
||||
current_runtime,
|
||||
];
|
||||
let current_ids = HashSet::from(["runtime:current"]);
|
||||
|
||||
let (compactable, retained) = auto_compaction_partition(&messages, ¤t_ids);
|
||||
|
||||
assert_eq!(compactable, vec![first_context, old_runtime]);
|
||||
assert_eq!(retained, Some(latest_context));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_image_is_loaded_only_for_the_provider_projection() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
|
||||
Reference in New Issue
Block a user