use crate::error::{Error, Result};
use crate::http::client::Client;
use crate::model::{
AssistantMessage, ContentBlock, Message, StopReason, TextContent, ThinkingContent, Usage,
UserContent,
};
use crate::provider::{Context, Provider, StreamEvent, StreamOptions};
use async_trait::async_trait;
use chrono::Utc;
use futures::stream::{self, Stream, StreamExt};
use std::collections::VecDeque;
use std::pin::Pin;
use uuid::Uuid;
pub const CURSOR_API_URL: &str = "https://api2.cursor.sh/agent.v1.AgentService/Run";
const CURSOR_CLIENT_VERSION: &str = "cli-2026.01.09-231024f";
const CURSOR_API_NAME: &str = "cursor-agent";
mod pb {
const WIRE_VARINT: u64 = 0;
const WIRE_I64: u64 = 1;
const WIRE_LEN: u64 = 2;
const WIRE_I32: u64 = 5;
#[allow(clippy::cast_possible_truncation)] pub fn write_varint(out: &mut Vec<u8>, mut value: u64) {
loop {
let byte = (value & 0x7f) as u8;
value >>= 7;
if value == 0 {
out.push(byte);
return;
}
out.push(byte | 0x80);
}
}
fn write_tag(out: &mut Vec<u8>, field: u32, wire: u64) {
write_varint(out, (u64::from(field) << 3) | wire);
}
pub fn write_len_delim(out: &mut Vec<u8>, field: u32, data: &[u8]) {
write_tag(out, field, WIRE_LEN);
write_varint(out, data.len() as u64);
out.extend_from_slice(data);
}
pub fn write_string(out: &mut Vec<u8>, field: u32, value: &str) {
write_len_delim(out, field, value.as_bytes());
}
#[derive(Clone, Copy)]
pub enum Field<'a> {
Varint(u64),
Len(&'a [u8]),
I64(u64),
I32(u32),
}
pub struct Reader<'a> {
buf: &'a [u8],
pos: usize,
}
impl<'a> Reader<'a> {
pub const fn new(buf: &'a [u8]) -> Self {
Self { buf, pos: 0 }
}
fn read_varint(&mut self) -> Option<u64> {
let mut result: u64 = 0;
let mut shift: u32 = 0;
loop {
let byte = *self.buf.get(self.pos)?;
self.pos += 1;
result |= u64::from(byte & 0x7f) << shift;
if byte & 0x80 == 0 {
return Some(result);
}
shift += 7;
if shift >= 64 {
return None; }
}
}
pub fn next_field(&mut self) -> Option<(u32, Field<'a>)> {
if self.pos >= self.buf.len() {
return None;
}
let tag = self.read_varint()?;
let field = u32::try_from(tag >> 3).ok()?;
match tag & 0x7 {
WIRE_VARINT => Some((field, Field::Varint(self.read_varint()?))),
WIRE_LEN => {
let len = usize::try_from(self.read_varint()?).ok()?;
let end = self.pos.checked_add(len)?;
let slice = self.buf.get(self.pos..end)?;
self.pos = end;
Some((field, Field::Len(slice)))
}
WIRE_I64 => {
let end = self.pos.checked_add(8)?;
let bytes = self.buf.get(self.pos..end)?;
self.pos = end;
Some((
field,
Field::I64(u64::from_le_bytes(bytes.try_into().ok()?)),
))
}
WIRE_I32 => {
let end = self.pos.checked_add(4)?;
let bytes = self.buf.get(self.pos..end)?;
self.pos = end;
Some((
field,
Field::I32(u32::from_le_bytes(bytes.try_into().ok()?)),
))
}
_ => None, }
}
}
}
const CONNECT_END_STREAM_FLAG: u8 = 0b0000_0010;
const CONNECT_COMPRESSED_FLAG: u8 = 0b0000_0001;
const MAX_CONNECT_FRAME_LEN: usize = 64 * 1024 * 1024;
#[allow(clippy::cast_possible_truncation)] fn encode_frame(flags: u8, payload: &[u8]) -> Vec<u8> {
let mut frame = Vec::with_capacity(5 + payload.len());
frame.push(flags);
frame.extend_from_slice(&(payload.len() as u32).to_be_bytes());
frame.extend_from_slice(payload);
frame
}
#[derive(Debug)]
struct ConnectFrame {
end_stream: bool,
compressed: bool,
payload: Vec<u8>,
}
#[derive(Default)]
struct FrameBuffer {
buf: Vec<u8>,
}
impl FrameBuffer {
fn push(&mut self, chunk: &[u8]) {
self.buf.extend_from_slice(chunk);
}
fn next_frame(&mut self) -> Option<Result<ConnectFrame>> {
if self.buf.len() < 5 {
return None;
}
let flags = self.buf[0];
let len = u32::from_be_bytes([self.buf[1], self.buf[2], self.buf[3], self.buf[4]]) as usize;
if len > MAX_CONNECT_FRAME_LEN {
return Some(Err(Error::api(format!(
"Cursor stream frame length {len} exceeds the maximum of {MAX_CONNECT_FRAME_LEN} bytes"
))));
}
let total = 5usize.checked_add(len)?;
if self.buf.len() < total {
return None;
}
let payload = self.buf[5..total].to_vec();
self.buf.drain(0..total);
Some(Ok(ConnectFrame {
end_stream: flags & CONNECT_END_STREAM_FLAG != 0,
compressed: flags & CONNECT_COMPRESSED_FLAG != 0,
payload,
}))
}
}
fn build_run_request(
model_id: &str,
system_prompt: Option<&str>,
user_text: &str,
conversation_id: &str,
message_id: &str,
) -> Vec<u8> {
let mut user_message = Vec::new();
pb::write_string(&mut user_message, 1, user_text);
pb::write_string(&mut user_message, 2, message_id);
let mut user_message_action = Vec::new();
pb::write_len_delim(&mut user_message_action, 1, &user_message);
let mut action = Vec::new();
pb::write_len_delim(&mut action, 1, &user_message_action);
let mut model_details = Vec::new();
pb::write_string(&mut model_details, 1, model_id);
let mut run_request = Vec::new();
pb::write_len_delim(&mut run_request, 2, &action);
pb::write_len_delim(&mut run_request, 3, &model_details);
pb::write_string(&mut run_request, 5, conversation_id);
if let Some(system) = system_prompt {
if !system.is_empty() {
pb::write_string(&mut run_request, 8, system);
}
}
let mut message = Vec::new();
pb::write_len_delim(&mut message, 1, &run_request);
message
}
fn latest_user_text(messages: &[Message]) -> Option<String> {
messages.iter().rev().find_map(|message| match message {
Message::User(user) => Some(render_user_content(&user.content)),
_ => None,
})
}
fn render_user_content(content: &UserContent) -> String {
match content {
UserContent::Text(text) => text.clone(),
UserContent::Blocks(blocks) => {
let mut out = String::new();
for block in blocks {
if let ContentBlock::Text(text) = block {
if !out.is_empty() {
out.push('\n');
}
out.push_str(&text.text);
}
}
out
}
}
}
#[derive(Debug, PartialEq, Eq)]
enum ServerEvent {
TextDelta(String),
ThinkingDelta(String),
TokenDelta(i64),
TurnEnded,
UsedTokens(u64),
}
fn decode_server_message(bytes: &[u8]) -> Vec<ServerEvent> {
let mut events = Vec::new();
let mut reader = pb::Reader::new(bytes);
while let Some((field, value)) = reader.next_field() {
match (field, value) {
(1, pb::Field::Len(inner)) => decode_interaction_update(inner, &mut events),
(3, pb::Field::Len(inner)) => {
if let Some(used) = decode_checkpoint_used_tokens(inner) {
events.push(ServerEvent::UsedTokens(used));
}
}
_ => {}
}
}
events
}
fn decode_interaction_update(bytes: &[u8], events: &mut Vec<ServerEvent>) {
let mut reader = pb::Reader::new(bytes);
while let Some((field, value)) = reader.next_field() {
match (field, value) {
(1, pb::Field::Len(inner)) => {
if let Some(text) = decode_delta_text(inner) {
events.push(ServerEvent::TextDelta(text));
}
}
(4, pb::Field::Len(inner)) => {
if let Some(text) = decode_delta_text(inner) {
events.push(ServerEvent::ThinkingDelta(text));
}
}
(8, pb::Field::Len(inner)) => {
if let Some(tokens) = decode_token_delta(inner) {
events.push(ServerEvent::TokenDelta(tokens));
}
}
(14, _) => events.push(ServerEvent::TurnEnded),
_ => {}
}
}
}
fn decode_delta_text(bytes: &[u8]) -> Option<String> {
let mut reader = pb::Reader::new(bytes);
while let Some((field, value)) = reader.next_field() {
if let (1, pb::Field::Len(raw)) = (field, value) {
return Some(String::from_utf8_lossy(raw).into_owned());
}
}
None
}
#[allow(clippy::cast_possible_truncation, clippy::cast_possible_wrap)]
fn decode_token_delta(bytes: &[u8]) -> Option<i64> {
let mut reader = pb::Reader::new(bytes);
while let Some((field, value)) = reader.next_field() {
if let (1, pb::Field::Varint(raw)) = (field, value) {
return Some(i64::from(raw as i32));
}
}
None
}
fn decode_checkpoint_used_tokens(bytes: &[u8]) -> Option<u64> {
let mut reader = pb::Reader::new(bytes);
while let Some((field, value)) = reader.next_field() {
if let (5, pb::Field::Len(token_details)) = (field, value) {
let mut inner = pb::Reader::new(token_details);
while let Some((f, v)) = inner.next_field() {
if let (1, pb::Field::Varint(used)) = (f, v) {
return Some(used);
}
}
}
}
None
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum BlockKind {
Text,
Thinking,
}
struct StreamState {
source: Pin<Box<dyn Stream<Item = std::io::Result<Vec<u8>>> + Send>>,
frames: FrameBuffer,
partial: AssistantMessage,
pending: VecDeque<StreamEvent>,
open_block: Option<(usize, BlockKind)>,
output_tokens: u64,
used_tokens: Option<u64>,
error_message: Option<String>,
started: bool,
finalized: bool,
done: bool,
transient_error_count: usize,
}
impl StreamState {
fn new(
source: Pin<Box<dyn Stream<Item = std::io::Result<Vec<u8>>> + Send>>,
partial: AssistantMessage,
) -> Self {
Self {
source,
frames: FrameBuffer::default(),
partial,
pending: VecDeque::new(),
open_block: None,
output_tokens: 0,
used_tokens: None,
error_message: None,
started: false,
finalized: false,
done: false,
transient_error_count: 0,
}
}
fn ensure_started(&mut self) {
if !self.started {
self.started = true;
self.pending.push_back(StreamEvent::Start {
partial: self.partial.clone(),
});
}
}
fn close_open_block(&mut self) {
let Some((index, kind)) = self.open_block.take() else {
return;
};
match kind {
BlockKind::Text => {
let content = match self.partial.content.get(index) {
Some(ContentBlock::Text(t)) => t.text.clone(),
_ => String::new(),
};
self.pending.push_back(StreamEvent::TextEnd {
content_index: index,
content,
});
}
BlockKind::Thinking => {
let content = match self.partial.content.get(index) {
Some(ContentBlock::Thinking(t)) => t.thinking.clone(),
_ => String::new(),
};
self.pending.push_back(StreamEvent::ThinkingEnd {
content_index: index,
content,
});
}
}
}
fn ensure_block(&mut self, kind: BlockKind) -> usize {
if let Some((index, open_kind)) = self.open_block {
if open_kind == kind {
return index;
}
self.close_open_block();
}
let index = self.partial.content.len();
match kind {
BlockKind::Text => {
self.partial
.content
.push(ContentBlock::Text(TextContent::new("")));
self.pending.push_back(StreamEvent::TextStart {
content_index: index,
});
}
BlockKind::Thinking => {
self.partial
.content
.push(ContentBlock::Thinking(ThinkingContent {
thinking: String::new(),
thinking_signature: None,
}));
self.pending.push_back(StreamEvent::ThinkingStart {
content_index: index,
});
}
}
self.open_block = Some((index, kind));
index
}
fn apply(&mut self, event: ServerEvent) {
match event {
ServerEvent::TextDelta(delta) => {
if delta.is_empty() {
return;
}
self.ensure_started();
let index = self.ensure_block(BlockKind::Text);
if let Some(ContentBlock::Text(text)) = self.partial.content.get_mut(index) {
text.text.push_str(&delta);
}
self.pending.push_back(StreamEvent::TextDelta {
content_index: index,
delta,
});
}
ServerEvent::ThinkingDelta(delta) => {
if delta.is_empty() {
return;
}
self.ensure_started();
let index = self.ensure_block(BlockKind::Thinking);
if let Some(ContentBlock::Thinking(thinking)) = self.partial.content.get_mut(index)
{
thinking.thinking.push_str(&delta);
}
self.pending.push_back(StreamEvent::ThinkingDelta {
content_index: index,
delta,
});
}
ServerEvent::TokenDelta(tokens) => {
if tokens > 0 {
self.output_tokens = self
.output_tokens
.saturating_add(u64::try_from(tokens).unwrap_or(0));
}
}
ServerEvent::UsedTokens(used) => {
self.used_tokens = Some(used);
}
ServerEvent::TurnEnded => {
}
}
}
fn process_frame(&mut self, frame: &ConnectFrame) -> Result<()> {
if frame.compressed {
return Err(Error::api(
"Cursor stream sent a compressed Connect frame; compression is not supported",
));
}
if frame.end_stream {
if let Ok(value) = serde_json::from_slice::<serde_json::Value>(&frame.payload) {
if let Some(error) = value.get("error") {
let message = error
.get("message")
.and_then(serde_json::Value::as_str)
.unwrap_or("Cursor stream returned an error")
.to_string();
self.error_message = Some(message);
}
}
self.finalize();
self.done = true;
return Ok(());
}
for event in decode_server_message(&frame.payload) {
self.apply(event);
}
Ok(())
}
fn finalize(&mut self) {
if self.finalized {
return;
}
self.finalized = true;
self.ensure_started();
self.close_open_block();
let output = self.output_tokens;
let total = self.used_tokens.unwrap_or(output);
let input = total.saturating_sub(output);
self.partial.usage = Usage {
input,
output,
cache_read: 0,
cache_write: 0,
total_tokens: total,
..Usage::default()
};
if let Some(message) = self.error_message.take() {
self.partial.stop_reason = StopReason::Error;
self.partial.error_message = Some(message);
self.pending.push_back(StreamEvent::Error {
reason: StopReason::Error,
error: self.partial.clone(),
});
} else {
self.partial.stop_reason = StopReason::Stop;
self.pending.push_back(StreamEvent::Done {
reason: StopReason::Stop,
message: self.partial.clone(),
});
}
}
async fn next_event(&mut self) -> Option<Result<StreamEvent>> {
loop {
if let Some(event) = self.pending.pop_front() {
return Some(Ok(event));
}
if self.done {
return None;
}
match self.frames.next_frame() {
Some(Ok(frame)) => {
if let Err(err) = self.process_frame(&frame) {
self.done = true;
return Some(Err(err));
}
continue;
}
Some(Err(err)) => {
self.done = true;
return Some(Err(err));
}
None => {}
}
match self.source.next().await {
Some(Ok(chunk)) => {
self.transient_error_count = 0;
self.frames.push(&chunk);
}
Some(Err(err)) => {
const MAX_CONSECUTIVE_TRANSIENT_ERRORS: usize = 5;
if matches!(
err.kind(),
std::io::ErrorKind::WriteZero
| std::io::ErrorKind::WouldBlock
| std::io::ErrorKind::TimedOut
) {
self.transient_error_count += 1;
if self.transient_error_count <= MAX_CONSECUTIVE_TRANSIENT_ERRORS {
continue;
}
}
self.done = true;
return Some(Err(Error::sse(&err)));
}
None => {
self.finalize();
self.done = true;
}
}
}
}
}
pub struct CursorProvider {
client: Client,
model: String,
base_url: String,
provider: String,
}
impl CursorProvider {
pub fn new(model: impl Into<String>) -> Self {
Self {
client: Client::new(),
model: model.into(),
base_url: CURSOR_API_URL.to_string(),
provider: "cursor".to_string(),
}
}
#[must_use]
pub fn with_provider_name(mut self, provider: impl Into<String>) -> Self {
self.provider = provider.into();
self
}
#[must_use]
pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
self.base_url = base_url.into();
self
}
#[must_use]
pub fn with_client(mut self, client: Client) -> Self {
self.client = client;
self
}
}
fn pick_token(inline: Option<&str>, env_token: Option<&str>) -> Option<String> {
inline
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
.or_else(|| {
env_token
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToString::to_string)
})
}
#[async_trait]
impl Provider for CursorProvider {
fn name(&self) -> &str {
&self.provider
}
fn api(&self) -> &str {
CURSOR_API_NAME
}
fn model_id(&self) -> &str {
&self.model
}
async fn stream(
&self,
context: &Context<'_>,
options: &StreamOptions,
) -> Result<Pin<Box<dyn Stream<Item = Result<StreamEvent>> + Send>>> {
let env_token = std::env::var("CURSOR_API_KEY")
.ok()
.or_else(|| std::env::var("CURSOR_ACCESS_TOKEN").ok());
let token =
pick_token(options.api_key.as_deref(), env_token.as_deref()).ok_or_else(|| {
Error::provider(
&self.provider,
"Missing Cursor credentials. Run /login cursor or set CURSOR_API_KEY.",
)
})?;
let user_text = latest_user_text(&context.messages)
.ok_or_else(|| Error::provider(&self.provider, "No user message to send to Cursor"))?;
let system_prompt = context.system_prompt.as_deref();
let conversation_id = Uuid::new_v4().to_string();
let message_id = Uuid::new_v4().to_string();
let request_id = Uuid::new_v4().to_string();
let request_body = build_run_request(
&self.model,
system_prompt,
&user_text,
&conversation_id,
&message_id,
);
let framed = encode_frame(0, &request_body);
let request = self
.client
.post(&self.base_url)
.header("content-type", "application/connect+proto")
.header("connect-protocol-version", "1")
.header("authorization", format!("Bearer {token}"))
.header("x-ghost-mode", "true")
.header("x-cursor-client-type", "cli")
.header("x-cursor-client-version", CURSOR_CLIENT_VERSION)
.header("x-request-id", request_id)
.body(framed);
let response = Box::pin(request.send()).await?;
let status = response.status();
if !(200..300).contains(&status) {
let body = response
.text()
.await
.unwrap_or_else(|err| format!("<failed to read body: {err}>"));
return Err(Error::provider(
&self.provider,
format!("Cursor API error (HTTP {status}): {body}"),
));
}
let partial = AssistantMessage {
api: CURSOR_API_NAME.to_string(),
provider: self.provider.clone(),
model: self.model.clone(),
timestamp: Utc::now().timestamp_millis(),
..AssistantMessage::default()
};
let state = StreamState::new(response.bytes_stream(), partial);
let stream = stream::unfold(state, |mut state| async move {
state.next_event().await.map(|event| (event, state))
});
Ok(Box::pin(stream))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn varint_round_trip() {
for value in [
0u64,
1,
127,
128,
300,
16_384,
u64::from(u32::MAX),
u64::MAX,
] {
let mut buf = Vec::new();
pb::write_varint(&mut buf, value);
let mut reader = pb::Reader::new(&buf);
let mut tagged = Vec::new();
pb::write_varint(&mut tagged, 1u64 << 3); tagged.extend_from_slice(&buf);
let mut r2 = pb::Reader::new(&tagged);
match r2.next_field() {
Some((1, pb::Field::Varint(got))) => assert_eq!(got, value),
other => panic!("expected varint {value}, got {:?}", other.map(|(f, _)| f)),
}
let _ = reader.next_field();
}
}
#[test]
fn reader_skips_unknown_fields() {
let mut buf = Vec::new();
pb::write_string(&mut buf, 2, "second");
pb::write_string(&mut buf, 1, "first");
let mut reader = pb::Reader::new(&buf);
let mut seen = Vec::new();
while let Some((field, value)) = reader.next_field() {
if let pb::Field::Len(raw) = value {
seen.push((field, String::from_utf8_lossy(raw).into_owned()));
}
}
assert_eq!(
seen,
vec![(2, "second".to_string()), (1, "first".to_string())]
);
}
#[test]
fn frame_encode_then_parse_across_chunks() {
let mut a = encode_frame(0, b"hello");
let b = encode_frame(CONNECT_END_STREAM_FLAG, b"{}");
a.extend_from_slice(&b);
let mut fb = FrameBuffer::default();
let mut frames = Vec::new();
for byte in &a {
fb.push(&[*byte]);
while let Some(frame) = fb.next_frame() {
frames.push(frame.expect("valid frame"));
}
}
assert_eq!(frames.len(), 2);
assert!(!frames[0].end_stream);
assert_eq!(frames[0].payload, b"hello");
assert!(frames[1].end_stream);
assert_eq!(frames[1].payload, b"{}");
}
#[test]
fn frame_length_prefix_is_big_endian() {
let frame = encode_frame(0, &[0xAB, 0xCD]);
assert_eq!(&frame[0..5], &[0x00, 0x00, 0x00, 0x00, 0x02]);
}
#[test]
fn frame_over_max_length_is_error_not_unbounded_buffering() {
let mut header = vec![0u8]; let over = u32::try_from(MAX_CONNECT_FRAME_LEN).expect("cap fits in u32") + 1;
header.extend_from_slice(&over.to_be_bytes());
let mut fb = FrameBuffer::default();
fb.push(&header);
match fb.next_frame() {
Some(Err(_)) => {}
other => panic!("expected Some(Err(..)) for over-cap frame length, got {other:?}"),
}
}
#[test]
fn frame_at_max_length_boundary_is_accepted() {
let payload = vec![0x7u8; 3];
let mut fb = FrameBuffer::default();
fb.push(&encode_frame(0, &payload));
let frame = fb
.next_frame()
.expect("frame buffered")
.expect("within cap");
assert_eq!(frame.payload, payload);
}
fn descend(bytes: &[u8], field: u32) -> Option<&[u8]> {
let mut reader = pb::Reader::new(bytes);
while let Some((f, value)) = reader.next_field() {
if f == field {
if let pb::Field::Len(inner) = value {
return Some(inner);
}
}
}
None
}
fn read_string(bytes: &[u8], field: u32) -> Option<String> {
let mut reader = pb::Reader::new(bytes);
while let Some((f, value)) = reader.next_field() {
if f == field {
if let pb::Field::Len(raw) = value {
return Some(String::from_utf8_lossy(raw).into_owned());
}
}
}
None
}
#[test]
fn build_run_request_round_trips_all_fields() {
let bytes = build_run_request(
"claude-4.5-sonnet",
Some("You are Pi."),
"hello cursor",
"conv-123",
"msg-456",
);
let run_request = descend(&bytes, 1).expect("run_request");
let model_details = descend(run_request, 3).expect("model_details");
assert_eq!(
read_string(model_details, 1).as_deref(),
Some("claude-4.5-sonnet")
);
assert_eq!(read_string(run_request, 5).as_deref(), Some("conv-123"));
assert_eq!(read_string(run_request, 8).as_deref(), Some("You are Pi."));
let action = descend(run_request, 2).expect("action");
let uma = descend(action, 1).expect("user_message_action");
let user_message = descend(uma, 1).expect("user_message");
assert_eq!(
read_string(user_message, 1).as_deref(),
Some("hello cursor")
);
assert_eq!(read_string(user_message, 2).as_deref(), Some("msg-456"));
}
#[test]
fn build_run_request_omits_empty_system_prompt() {
let bytes = build_run_request("m", Some(""), "hi", "c", "msg");
let run_request = descend(&bytes, 1).expect("run_request");
assert!(read_string(run_request, 8).is_none());
let bytes_none = build_run_request("m", None, "hi", "c", "msg");
let rr = descend(&bytes_none, 1).expect("run_request");
assert!(read_string(rr, 8).is_none());
}
#[test]
fn latest_user_text_prefers_last_user_message() {
use crate::model::{AssistantMessage, UserMessage};
use std::sync::Arc;
let messages = vec![
Message::User(UserMessage {
content: UserContent::Text("first".into()),
timestamp: 0,
}),
Message::Assistant(Arc::new(AssistantMessage::default())),
Message::User(UserMessage {
content: UserContent::Text("second".into()),
timestamp: 0,
}),
];
assert_eq!(latest_user_text(&messages).as_deref(), Some("second"));
}
#[test]
fn latest_user_text_joins_text_blocks() {
use crate::model::UserMessage;
let messages = vec![Message::User(UserMessage {
content: UserContent::Blocks(vec![
ContentBlock::Text(TextContent::new("line one")),
ContentBlock::Text(TextContent::new("line two")),
]),
timestamp: 0,
})];
assert_eq!(
latest_user_text(&messages).as_deref(),
Some("line one\nline two")
);
}
fn interaction_update(field: u32, text: &str) -> Vec<u8> {
let mut delta = Vec::new();
pb::write_string(&mut delta, 1, text); let mut update = Vec::new();
pb::write_len_delim(&mut update, field, &delta);
let mut server = Vec::new();
pb::write_len_delim(&mut server, 1, &update); server
}
fn token_delta_message(tokens: u64) -> Vec<u8> {
let mut td = Vec::new();
pb::write_varint(&mut td, 1u64 << 3); pb::write_varint(&mut td, tokens);
let mut update = Vec::new();
pb::write_len_delim(&mut update, 8, &td); let mut server = Vec::new();
pb::write_len_delim(&mut server, 1, &update);
server
}
fn checkpoint_message(used: u64) -> Vec<u8> {
let mut details = Vec::new();
pb::write_varint(&mut details, 1u64 << 3); pb::write_varint(&mut details, used);
let mut state = Vec::new();
pb::write_len_delim(&mut state, 5, &details); let mut server = Vec::new();
pb::write_len_delim(&mut server, 3, &state); server
}
#[test]
fn decode_text_and_thinking_deltas() {
assert_eq!(
decode_server_message(&interaction_update(1, "hi")),
vec![ServerEvent::TextDelta("hi".into())]
);
assert_eq!(
decode_server_message(&interaction_update(4, "pondering")),
vec![ServerEvent::ThinkingDelta("pondering".into())]
);
}
#[test]
fn decode_token_and_checkpoint() {
assert_eq!(
decode_server_message(&token_delta_message(7)),
vec![ServerEvent::TokenDelta(7)]
);
assert_eq!(
decode_server_message(&checkpoint_message(1234)),
vec![ServerEvent::UsedTokens(1234)]
);
}
fn drive(frames: &[Vec<u8>], end_stream_json: &[u8]) -> Vec<StreamEvent> {
let empty: Pin<Box<dyn Stream<Item = std::io::Result<Vec<u8>>> + Send>> =
Box::pin(stream::empty());
let mut state = StreamState::new(empty, AssistantMessage::default());
for frame in frames {
state
.process_frame(&ConnectFrame {
end_stream: false,
compressed: false,
payload: frame.clone(),
})
.expect("data frame");
}
state
.process_frame(&ConnectFrame {
end_stream: true,
compressed: false,
payload: end_stream_json.to_vec(),
})
.expect("end frame");
state.pending.drain(..).collect()
}
#[test]
fn stream_state_emits_ordered_text_events() {
let events = drive(
&[
interaction_update(1, "Hello"),
interaction_update(1, ", world"),
token_delta_message(5),
checkpoint_message(42),
],
b"{}",
);
let mut it = events.iter();
assert!(matches!(it.next(), Some(StreamEvent::Start { .. })));
assert!(matches!(
it.next(),
Some(StreamEvent::TextStart { content_index: 0 })
));
assert!(matches!(
it.next(),
Some(StreamEvent::TextDelta { delta, .. }) if delta == "Hello"
));
assert!(matches!(
it.next(),
Some(StreamEvent::TextDelta { delta, .. }) if delta == ", world"
));
assert!(matches!(
it.next(),
Some(StreamEvent::TextEnd { content, .. }) if content == "Hello, world"
));
match it.next() {
Some(StreamEvent::Done { reason, message }) => {
assert_eq!(*reason, StopReason::Stop);
assert_eq!(message.usage.output, 5);
assert_eq!(message.usage.total_tokens, 42);
assert_eq!(message.usage.input, 37);
assert_eq!(message.usage.cache_read, 0);
}
other => panic!("expected Done, got {other:?}"),
}
assert!(it.next().is_none());
}
#[test]
fn stream_state_switches_thinking_to_text_block() {
let events = drive(
&[
interaction_update(4, "let me think"),
interaction_update(1, "answer"),
],
b"{}",
);
assert!(matches!(events[0], StreamEvent::Start { .. }));
assert!(matches!(
events[1],
StreamEvent::ThinkingStart { content_index: 0 }
));
assert!(matches!(events[2], StreamEvent::ThinkingDelta { .. }));
assert!(matches!(
events[3],
StreamEvent::ThinkingEnd {
content_index: 0,
..
}
));
assert!(matches!(
events[4],
StreamEvent::TextStart { content_index: 1 }
));
assert!(matches!(events[5], StreamEvent::TextDelta { .. }));
assert!(matches!(
events[6],
StreamEvent::TextEnd {
content_index: 1,
..
}
));
assert!(matches!(events[7], StreamEvent::Done { .. }));
}
#[test]
fn stream_state_surfaces_end_stream_error() {
let events = drive(
&[interaction_update(1, "partial")],
br#"{"error":{"message":"boom"}}"#,
);
match events.last() {
Some(StreamEvent::Error { reason, error }) => {
assert_eq!(*reason, StopReason::Error);
assert_eq!(error.error_message.as_deref(), Some("boom"));
}
other => panic!("expected Error, got {other:?}"),
}
}
#[test]
fn pick_token_precedence() {
assert_eq!(
pick_token(Some("inline"), Some("env")).as_deref(),
Some("inline")
);
assert_eq!(pick_token(Some(" "), Some("env")).as_deref(), Some("env"));
assert_eq!(pick_token(None, Some("env")).as_deref(), Some("env"));
assert_eq!(pick_token(None, None), None);
assert_eq!(pick_token(Some(""), Some("")), None);
}
#[test]
fn provider_reports_identity() {
let provider = CursorProvider::new("claude-4.5-sonnet");
assert_eq!(provider.name(), "cursor");
assert_eq!(provider.api(), CURSOR_API_NAME);
assert_eq!(provider.model_id(), "claude-4.5-sonnet");
}
}