use serde_json::Value; use crate::{ cursor::{interaction, proto::agent::v1 as pb}, model::{ToolCall, ToolResult}, Error, Result, }; use super::{now_ms, ToolCompletion}; const SUBAGENTS_DISABLED_REMINDER: &str = "The user has disabled the subagent model. Please remind the user to enable it in Cursor Settings → Models → Explore Subagent Model."; pub(crate) fn local(call: &ToolCall, message_index: usize) -> Result { match normalized(&call.name).as_str() { "todowrite" => todo_write(call), "updatecurrentstep" => update_current_step(call, message_index), _ => Err(Error::Protocol(format!("unsupported tool: {}", call.name))), } } pub(crate) fn subagents_disabled(call: &ToolCall) -> Result { let mut rendered = interaction::render_tool_call(call, false)?; let Some(pb::tool_call::Tool::TaskToolCall(tool)) = rendered.tool.as_mut() else { return Err(Error::Protocol("Task has no Cursor representation".into())); }; tool.result = Some(pb::TaskResult { result: Some(pb::task_result::Result::Error(pb::TaskError { error: SUBAGENTS_DISABLED_REMINDER.into(), })), }); let tool = rendered .tool .ok_or_else(|| Error::Protocol("Task has no Cursor representation".into()))?; Ok(ToolCompletion::new( call, now_ms(), ToolResult { call_id: call.call_id.clone(), content: SUBAGENTS_DISABLED_REMINDER.into(), is_error: true, image: None, }, tool, )) } fn todo_write(call: &ToolCall) -> Result { let todos = todo_items(&call.arguments); let total_count = todos.len() as i32; let was_merge = call .arguments .get("merge") .and_then(Value::as_bool) .unwrap_or(false); let mut rendered = interaction::render_tool_call(call, false)?; let Some(pb::tool_call::Tool::UpdateTodosToolCall(tool)) = rendered.tool.as_mut() else { return Err(Error::Protocol( "TodoWrite has no Cursor representation".into(), )); }; tool.result = Some(pb::UpdateTodosResult { result: Some(pb::update_todos_result::Result::Success( pb::UpdateTodosSuccess { todos, total_count, was_merge, }, )), }); let tool = rendered .tool .ok_or_else(|| Error::Protocol("TodoWrite has no Cursor representation".into()))?; Ok(ToolCompletion::new( call, now_ms(), ToolResult { call_id: call.call_id.clone(), content: call.arguments.to_string(), is_error: false, image: None, }, tool, )) } fn update_current_step(call: &ToolCall, message_index: usize) -> Result { let current_step = call .arguments .get("current_step") .and_then(Value::as_str) .unwrap_or_default() .to_string(); let mut rendered = interaction::render_tool_call(call, false)?; let Some(pb::tool_call::Tool::CommunicateUpdateToolCall(tool)) = rendered.tool.as_mut() else { return Err(Error::Protocol( "UpdateCurrentStep has no Cursor representation".into(), )); }; let message_index = u32::try_from(message_index) .map_err(|_| Error::Protocol("Cursor message index space exhausted".into()))?; tool.result = Some(pb::CommunicateUpdateResult { result: Some(pb::communicate_update_result::Result::Success( pb::CommunicateUpdateSuccess { current_step: current_step.clone(), message_index, }, )), }); let tool = rendered .tool .ok_or_else(|| Error::Protocol("UpdateCurrentStep has no Cursor representation".into()))?; Ok(ToolCompletion::new( call, now_ms(), ToolResult { call_id: call.call_id.clone(), content: serde_json::json!({ "success": { "current_step": current_step, "message_index": message_index, } }) .to_string(), is_error: false, image: None, }, tool, )) } pub(crate) fn todo_items(arguments: &Value) -> Vec { arguments .get("todos") .and_then(Value::as_array) .into_iter() .flatten() .map(|todo| pb::TodoItem { id: text(todo, "id"), content: text(todo, "content"), status: match todo .get("status") .and_then(Value::as_str) .unwrap_or("pending") { "in_progress" => pb::TodoStatus::InProgress as i32, "completed" => pb::TodoStatus::Completed as i32, "cancelled" => pb::TodoStatus::Cancelled as i32, _ => pb::TodoStatus::Pending as i32, }, created_at: 0, updated_at: 0, dependencies: todo .get("dependencies") .and_then(Value::as_array) .into_iter() .flatten() .filter_map(Value::as_str) .map(str::to_string) .collect(), }) .collect() } fn text(value: &Value, name: &str) -> String { value .get(name) .and_then(Value::as_str) .unwrap_or_default() .into() } #[cfg(test)] mod tests { use super::*; #[test] fn disabled_subagent_returns_a_model_visible_system_reminder() { let arguments = serde_json::json!({ "description":"inspect", "prompt":"inspect", "subagent_type":"explore" }); let completion = subagents_disabled(&ToolCall { index: 0, call_id: "task-1".into(), model_call_id: "model-call-1".into(), name: "Task".into(), arguments_text: arguments.to_string(), arguments, }) .unwrap(); assert_eq!(completion.result().content, SUBAGENTS_DISABLED_REMINDER); assert!(completion.result().is_error); assert!(matches!( completion.tool_call().tool.as_ref(), Some(pb::tool_call::Tool::TaskToolCall(pb::TaskToolCall { result: Some(pb::TaskResult { result: Some(pb::task_result::Result::Error(pb::TaskError { error })), }), .. })) if error == SUBAGENTS_DISABLED_REMINDER )); } } fn normalized(name: &str) -> String { name.chars() .filter(|character| character.is_ascii_alphanumeric()) .flat_map(char::to_lowercase) .collect() }