mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-04 02:52:55 +08:00
stabilize cursor request context and release workflow
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are now in Agent mode. You have EXITED your previous mode. Continue with the task in the new mode.
|
||||
</system_reminder>
|
||||
<system_reminder>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are now in Ask mode. You have EXITED your previous mode. Continue with the task in the new mode.
|
||||
</system_reminder>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<timestamp>{{TIMESTAMP}}</timestamp>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<timestamp>{{TIMESTAMP}}</timestamp>
|
||||
<user_query>
|
||||
{{USER_QUERY}}
|
||||
</user_query>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are now in Debug mode. You have EXITED your previous mode. Continue with the task in the new mode.
|
||||
</system_reminder>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are now in Multitask mode. You have EXITED your previous mode. Continue with the task in the new mode.
|
||||
</system_reminder>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are now in Plan mode. You have EXITED your previous mode. Continue with the task in the new mode.
|
||||
</system_reminder>
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{REQUEST_CONTEXT}}{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
{{OPEN_FILES}}{{SELECTED_CONTEXT}}{{ACTION_CONTEXT}}<system_reminder>
|
||||
You are currently working inside a Task subagent. Your parent agent has delegated a clearly bounded assignment to you. Complete that assignment directly with the tools available in this session. The Task tool is unavailable inside subagents, so delegation cannot be nested.
|
||||
</system_reminder>
|
||||
<timestamp>{{TIMESTAMP}}</timestamp>
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -323,7 +323,6 @@ fn every_captured_mode_owns_and_renders_its_runtime_template() {
|
||||
.unwrap(),
|
||||
);
|
||||
let values = BTreeMap::from([
|
||||
("REQUEST_CONTEXT", String::new()),
|
||||
("OPEN_FILES", String::new()),
|
||||
("SELECTED_CONTEXT", String::new()),
|
||||
("ACTION_CONTEXT", String::new()),
|
||||
|
||||
+137
-20
@@ -19,9 +19,9 @@ use cursor_server::{
|
||||
use prost::Message;
|
||||
|
||||
#[tokio::test]
|
||||
async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message() {
|
||||
async fn unchanged_request_context_is_not_repeated_and_preserves_the_provider_prefix() {
|
||||
let (_directory, store) = fixtures::temp_store().await;
|
||||
let references = references(&store).await;
|
||||
let first_references = references(&store).await;
|
||||
let provider = fake_provider::FakeProvider::default();
|
||||
provider.push(vec![
|
||||
ModelEvent::Start {
|
||||
@@ -32,6 +32,15 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
ModelEvent::TextEnd,
|
||||
ModelEvent::Done(FinishReason::Stop),
|
||||
]);
|
||||
provider.push(vec![
|
||||
ModelEvent::Start {
|
||||
model_call_id: "model-2".into(),
|
||||
},
|
||||
ModelEvent::TextStart,
|
||||
ModelEvent::TextDelta("answer again".into()),
|
||||
ModelEvent::TextEnd,
|
||||
ModelEvent::Done(FinishReason::Stop),
|
||||
]);
|
||||
let assets = PromptAssets::load(
|
||||
std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
|
||||
.join("prompt/cursor")
|
||||
@@ -39,7 +48,7 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
)
|
||||
.unwrap();
|
||||
let registry = CursorSessionRegistry::new(
|
||||
store,
|
||||
store.clone(),
|
||||
Arc::new(provider.clone()),
|
||||
PromptCompiler::new(assets),
|
||||
Default::default(),
|
||||
@@ -49,12 +58,13 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno: 0,
|
||||
message: Box::new(run_request(references)),
|
||||
message: Box::new(run_request(first_references)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut seqno = 1;
|
||||
let mut checkpoint = None;
|
||||
loop {
|
||||
let frame = tokio::time::timeout(std::time::Duration::from_secs(5), output.recv())
|
||||
.await
|
||||
@@ -65,15 +75,21 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
break;
|
||||
}
|
||||
let message = pb::AgentServerMessage::decode(payload).unwrap();
|
||||
if let Some(pb::agent_server_message::Message::KvServerMessage(kv)) = message.message {
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(kv_ack(kv.id)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
match message.message {
|
||||
Some(pb::agent_server_message::Message::KvServerMessage(kv)) => {
|
||||
handle
|
||||
.command(CursorCommand::Append {
|
||||
seqno,
|
||||
message: Box::new(kv_ack(kv.id)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
seqno += 1;
|
||||
}
|
||||
Some(pb::agent_server_message::Message::ConversationCheckpointUpdate(state)) => {
|
||||
checkpoint = Some(state);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,12 +105,23 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
.tools
|
||||
.iter()
|
||||
.any(|tool| tool.name == "GenerateImage"));
|
||||
assert_eq!(request.history.len(), 1);
|
||||
assert_eq!(request.history.len(), 2);
|
||||
assert!(request.history[0]
|
||||
.message_id
|
||||
.starts_with("request-context:"));
|
||||
let ProjectedContent::Parts(context_parts) = &request.history[0].content else {
|
||||
panic!("request context message must use typed parts")
|
||||
};
|
||||
let [ContentPart::Text { text: context_text }] = context_parts.as_slice() else {
|
||||
panic!("request context message must contain one text part")
|
||||
};
|
||||
assert_eq!(
|
||||
request.history[0].message_id,
|
||||
request.history[1].message_id,
|
||||
"runtime:run-request:ask-request"
|
||||
);
|
||||
let ProjectedContent::Parts(parts) = &request.history[0].content else {
|
||||
assert!(!request.prompt.instructions.contains("workspace rule"));
|
||||
assert!(!request.prompt.instructions.contains("<mcp_meta_tools>"));
|
||||
let ProjectedContent::Parts(parts) = &request.history[1].content else {
|
||||
panic!("runtime message must use typed parts")
|
||||
};
|
||||
let [ContentPart::Text { text }] = parts.as_slice() else {
|
||||
@@ -109,6 +136,15 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
"<definition_path>/tmp/mcp-test/lookup.json</definition_path>",
|
||||
"<input_schema>{"properties":{"query":{"type":"string"}},"type":"object"}</input_schema>",
|
||||
"Call a listed tool directly with CallMcpTool without calling GetMcpTools first.",
|
||||
] {
|
||||
assert!(
|
||||
context_text.contains(expected),
|
||||
"missing request context section: {expected}"
|
||||
);
|
||||
}
|
||||
assert!(!context_text.contains("complete skill body"));
|
||||
assert!(!context_text.contains("complete MCP server instructions"));
|
||||
for expected in [
|
||||
"Ask mode is active.",
|
||||
"<user_query>\nexplain this\n</user_query>",
|
||||
] {
|
||||
@@ -117,9 +153,65 @@ async fn current_mode_and_referenced_context_are_consumed_by_one_runtime_message
|
||||
"missing runtime section: {expected}"
|
||||
);
|
||||
}
|
||||
assert!(!text.contains("complete skill body"));
|
||||
assert!(!text.contains("complete MCP server instructions"));
|
||||
assert!(!text.contains("<rules>"));
|
||||
assert!(!text.contains("<mcp_meta_tools>"));
|
||||
assert!(text.contains("/workspace/src/main.rs"));
|
||||
|
||||
let second = registry.get_or_create("ask-request-2").await.unwrap();
|
||||
let mut second_output = second.subscribe();
|
||||
second
|
||||
.command(CursorCommand::Append {
|
||||
seqno: 0,
|
||||
message: Box::new(run_request_with_state(
|
||||
references(&store).await,
|
||||
checkpoint.expect("first Run must publish a checkpoint"),
|
||||
)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let mut second_seqno = 1;
|
||||
loop {
|
||||
let frame = tokio::time::timeout(std::time::Duration::from_secs(5), second_output.recv())
|
||||
.await
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
let (flags, payload) = connect::decode_frames(&frame).unwrap().pop().unwrap();
|
||||
if flags & connect::END_STREAM_FLAG != 0 {
|
||||
break;
|
||||
}
|
||||
let message = pb::AgentServerMessage::decode(payload).unwrap();
|
||||
if let Some(pb::agent_server_message::Message::KvServerMessage(kv)) = message.message {
|
||||
second
|
||||
.command(CursorCommand::Append {
|
||||
seqno: second_seqno,
|
||||
message: Box::new(kv_ack(kv.id)),
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
second_seqno += 1;
|
||||
}
|
||||
}
|
||||
|
||||
let requests = provider.requests();
|
||||
assert_eq!(requests.len(), 2);
|
||||
assert_eq!(
|
||||
requests[1].prompt.instructions, requests[0].prompt.instructions,
|
||||
"unchanged request context must not rewrite the system prompt"
|
||||
);
|
||||
assert_eq!(
|
||||
requests[1].history[..requests[0].history.len()],
|
||||
requests[0].history,
|
||||
"the previous provider history must remain an exact prefix"
|
||||
);
|
||||
assert_eq!(
|
||||
requests[1]
|
||||
.history
|
||||
.iter()
|
||||
.filter(|message| message.message_id.starts_with("request-context:"))
|
||||
.count(),
|
||||
1,
|
||||
"identical request context must not be appended again"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -252,10 +344,10 @@ async fn missing_context_parts_use_current_cursor_response_and_cache_its_content
|
||||
let requests = provider.requests();
|
||||
assert_eq!(requests.len(), 1);
|
||||
let ProjectedContent::Parts(parts) = &requests[0].history[0].content else {
|
||||
panic!("runtime message must use typed parts")
|
||||
panic!("request context message must use typed parts")
|
||||
};
|
||||
let [ContentPart::Text { text }] = parts.as_slice() else {
|
||||
panic!("this fixture has no images")
|
||||
panic!("request context message must contain one text part")
|
||||
};
|
||||
assert!(text.contains("<mcp_meta_tool_server name=\"live-mcp\" identifier=\"live-mcp\">"));
|
||||
assert!(text.contains("<mcp_tool name=\"current-tool\">"));
|
||||
@@ -449,6 +541,31 @@ fn run_request(references: References) -> pb::AgentClientMessage {
|
||||
}
|
||||
}
|
||||
|
||||
fn run_request_with_state(
|
||||
references: References,
|
||||
state: pb::ConversationStateStructure,
|
||||
) -> pb::AgentClientMessage {
|
||||
let mut message = run_request(references);
|
||||
let Some(pb::agent_client_message::Message::RunRequest(request)) = message.message.as_mut()
|
||||
else {
|
||||
unreachable!("run_request always returns a RunRequest")
|
||||
};
|
||||
request.conversation_state = Some(state);
|
||||
let Some(pb::conversation_action::Action::UserMessageAction(action)) = request
|
||||
.action
|
||||
.as_mut()
|
||||
.and_then(|action| action.action.as_mut())
|
||||
else {
|
||||
unreachable!("run_request always contains a UserMessageAction")
|
||||
};
|
||||
action
|
||||
.user_message
|
||||
.as_mut()
|
||||
.expect("run_request always contains a UserMessage")
|
||||
.message_id = "wire-user-2".into();
|
||||
message
|
||||
}
|
||||
|
||||
fn kv_ack(id: u32) -> pb::AgentClientMessage {
|
||||
pb::AgentClientMessage {
|
||||
message: Some(pb::agent_client_message::Message::KvClientMessage(
|
||||
|
||||
@@ -259,18 +259,27 @@ async fn text_turn_runs_from_bidi_request_through_checkpoint_and_end_stream() {
|
||||
.contains("powered by Test Model"));
|
||||
let projected = &requests[0].history;
|
||||
assert_eq!(projected[0].role, Role::User);
|
||||
let ProjectedContent::Parts(runtime) = &projected[0].content else {
|
||||
panic!("runtime context must be text")
|
||||
assert!(projected[0].message_id.starts_with("request-context:"));
|
||||
let ProjectedContent::Parts(context) = &projected[0].content else {
|
||||
panic!("request context must be text")
|
||||
};
|
||||
assert!(matches!(
|
||||
context.as_slice(),
|
||||
[cursor_server::model::ContentPart::Text { text }]
|
||||
if text.contains("<user_info>")
|
||||
));
|
||||
let ProjectedContent::Parts(runtime) = &projected[1].content else {
|
||||
panic!("runtime user message must be text")
|
||||
};
|
||||
assert!(matches!(
|
||||
runtime.as_slice(),
|
||||
[cursor_server::model::ContentPart::Text { text }]
|
||||
if text.contains("<user_info>")
|
||||
&& text.contains("<user_query>\nhello\n</user_query>")
|
||||
if text.contains("<user_query>\nhello\n</user_query>")
|
||||
&& !text.contains("<user_info>")
|
||||
));
|
||||
assert_eq!(
|
||||
projected.len(),
|
||||
1,
|
||||
2,
|
||||
"the raw UserMessage is not projected twice"
|
||||
);
|
||||
|
||||
@@ -278,9 +287,15 @@ async fn text_turn_runs_from_bidi_request_through_checkpoint_and_end_stream() {
|
||||
.load_current_messages(&cursor_server::model::ConversationId::new("conversation"))
|
||||
.await
|
||||
.unwrap();
|
||||
assert_eq!(messages[0].message_id, "runtime:run-request:request");
|
||||
assert!(messages[0].message_id.starts_with("request-context:"));
|
||||
assert_eq!(messages[0].role, Role::User);
|
||||
assert_eq!(messages.len(), 2, "runtime user plus final assistant");
|
||||
assert_eq!(messages[1].message_id, "runtime:run-request:request");
|
||||
assert_eq!(messages[1].role, Role::User);
|
||||
assert_eq!(
|
||||
messages.len(),
|
||||
3,
|
||||
"request context plus runtime user and final assistant"
|
||||
);
|
||||
let stored_runs: Vec<String> = sqlx::query_scalar("SELECT run_id FROM runs ORDER BY run_id")
|
||||
.fetch_all(store.pool())
|
||||
.await
|
||||
|
||||
Reference in New Issue
Block a user