Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 7 additions & 7 deletions n00n-agent/src/agent/tool_dispatch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ use crate::{AgentError, AgentEvent, ToolDoneEvent, ToolOutput, ToolStartEvent};
use n00n_config::ToolKey;

const SUBAGENT_PLUGINS: &[&str] = &["task", "workflow"];
const CANCELLED_SUBAGENT_OUTPUTS: &[&str] = &[
"cancelled",
"sub-agent error: cancelled",
"task failed: cancelled",
"task failed: plugin interrupted: task cancelled",
];

#[derive(Clone, Copy)]
pub enum Emit {
Expand Down Expand Up @@ -286,13 +292,7 @@ fn is_subagent_failure(event: &ToolDoneEvent, ctx: &ToolContext) -> bool {
}

fn is_cancelled_subagent_output(output: &str) -> bool {
matches!(
output.trim(),
"cancelled"
| "sub-agent error: cancelled"
| "task failed: cancelled"
| "task failed: plugin interrupted: task cancelled"
)
CANCELLED_SUBAGENT_OUTPUTS.contains(&output.trim())
}

pub(super) struct RecentCalls(VecDeque<(String, u64)>);
Expand Down
78 changes: 67 additions & 11 deletions n00n-providers/src/providers/cursor/run.rs
Original file line number Diff line number Diff line change
Expand Up @@ -723,15 +723,20 @@ fn handle_data_frame(
status: 502,
message,
})?;
if let Ok(Some(op)) = parse_kv_server_message(&payload) {
let bad_frame = |message| AgentError::Api {
status: 502,
message,
};

if let Some(op) = parse_kv_server_message(&payload).map_err(bad_frame)? {
queue_checkpoint_reply(op, checkpoints, outbound)?;
return Ok(FrameHandleOutcome {
exec_skipped: false,
text_deltas: 0,
kv_op: true,
});
}
if let Ok(true) = has_exec_server_message(&payload) {
if has_exec_server_message(&payload).map_err(bad_frame)? {
// Phase 0: n00n owns tools; ignore Cursor-side exec until Phase 1 maps them.
// Aborting the whole turn drops text deltas that often follow.
return Ok(FrameHandleOutcome {
Expand All @@ -741,16 +746,12 @@ fn handle_data_frame(
});
}
let mut deltas = 0u32;
if let Ok(text_deltas) = extract_text_deltas(&payload) {
for delta in text_deltas {
text.push_str(&delta);
deltas = deltas.saturating_add(1);
}
for delta in extract_text_deltas(&payload).map_err(bad_frame)? {
text.push_str(&delta);
deltas = deltas.saturating_add(1);
}
if let Ok(thinking_deltas) = extract_thinking_deltas(&payload) {
for delta in thinking_deltas {
thinking.push_str(&delta);
}
for delta in extract_thinking_deltas(&payload).map_err(bad_frame)? {
thinking.push_str(&delta);
}
Ok(FrameHandleOutcome {
exec_skipped: false,
Expand Down Expand Up @@ -841,6 +842,27 @@ mod tests {
assert!(outbound.lock().expect("lock").queue.is_empty());
}

#[test]
fn handle_data_frame_rejects_unknown_wire_type_three_payload() {
let frame = ConnectFrame {
end_stream: false,
compressed: false,
payload: vec![0x0b, 0x0c],
};
let store = shared_store();
let (outbound, _notify) = new_outbound_queue();
let mut text = String::new();
let mut thinking = String::new();

let Err(error) = handle_data_frame(&frame, &mut text, &mut thinking, &store, &outbound)
else {
panic!("unknown protobuf wire types must fail the frame");
};

assert!(matches!(error, AgentError::Api { status: 502, .. }));
assert!(error.to_string().contains("wire type 3"));
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#[test]
fn handle_data_frame_queues_set_blob_ack() {
let mut args = field_bytes(1, b"blob-id");
Expand Down Expand Up @@ -903,6 +925,7 @@ mod tests {
};

assert!(matches!(error, AgentError::Api { status: 502, .. }));
assert!(error.to_string().contains("gzip"));
}

#[test]
Expand Down Expand Up @@ -982,6 +1005,39 @@ mod tests {
);
}

#[test]
fn extract_text_deltas_rejects_invalid_protobuf() {
let malformed = [0x0b, 0x0c];
let err = extract_text_deltas(&malformed).expect_err("must fail");
assert!(err.contains("wire type 3"));

let truncated = [0x0a, 0x05, 0x01, 0x02];
let err = extract_text_deltas(&truncated).expect_err("must fail");
assert!(err.contains("truncated"));
}

#[test]
fn extract_thinking_deltas_rejects_invalid_protobuf() {
let malformed = [0x0b, 0x0c];
let err = extract_thinking_deltas(&malformed).expect_err("must fail");
assert!(err.contains("wire type 3"));

let truncated = [0x0a, 0x05, 0x01, 0x02];
let err = extract_thinking_deltas(&truncated).expect_err("must fail");
assert!(err.contains("truncated"));
}

#[test]
fn has_exec_server_message_rejects_invalid_protobuf() {
let malformed = [0x0b, 0x0c];
let err = has_exec_server_message(&malformed).expect_err("must fail");
assert!(err.contains("wire type 3"));

let truncated = [0x0a, 0x05, 0x01, 0x02];
let err = has_exec_server_message(&truncated).expect_err("must fail");
assert!(err.contains("truncated"));
}

#[test]
fn run_default_model_live() {
if !live_enabled() {
Expand Down
Loading