stabilize cursor request context and release workflow

This commit is contained in:
leookun
2026-08-24 03:44:56 +08:00
parent 2075b61763
commit b74291af79
20 changed files with 559 additions and 77 deletions
+5 -7
View File
@@ -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
+17
View File
@@ -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!({
-1
View File
@@ -134,7 +134,6 @@ impl PromptAssets {
}
const RUNTIME_VARIABLES: &[&str] = &[
"REQUEST_CONTEXT",
"OPEN_FILES",
"SELECTED_CONTEXT",
"ACTION_CONTEXT",
+35 -15
View File
@@ -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 -4
View File
@@ -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>",
)
));
}
}
+65 -7
View File
@@ -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, &current_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, &current_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();