mirror of
https://wget.la/https://github.com/leookun/cursor-byok
synced 2026-10-03 18:23:51 +08:00
feat: add Task tool functionality
- Introduced a new `Task` presentation type in the `ToolCallStream` to handle task-related projections. - Implemented the `TaskProjection` struct with fields for description, prompt, subagent type, model, resume, and environment. - Added a `project` method to `TaskProjection` to process task-related events and generate interaction updates. - Created a `task_partial` function to format task updates for the agent server message. - Included unit tests to verify the correct behavior of task description projections.
This commit is contained in:
@@ -12,7 +12,7 @@ use crate::{
|
||||
};
|
||||
|
||||
pub use query::tool_query;
|
||||
pub(crate) use render::{create_plan_partial, edit_content_delta, edit_path_partial};
|
||||
pub(crate) use render::{create_plan_partial, edit_content_delta, edit_path_partial, task_partial};
|
||||
pub use render::{dynamic_mcp_placeholder, render_dynamic_mcp, tool_completed};
|
||||
pub use request::{abort, mcp_request, mcp_state_request, request};
|
||||
pub(crate) use request::{edit_read_request, json_object_to_prost, mcp_meta_request};
|
||||
|
||||
@@ -54,6 +54,49 @@ pub(crate) fn edit_content_delta(call: &ToolCall, content: String) -> pb::AgentS
|
||||
)))
|
||||
}
|
||||
|
||||
pub(crate) fn task_partial(
|
||||
call: &ToolCall,
|
||||
description: &str,
|
||||
prompt: &str,
|
||||
subagent: &str,
|
||||
model: &str,
|
||||
resume: &str,
|
||||
environment: &str,
|
||||
) -> pb::AgentServerMessage {
|
||||
server_interaction(pb::interaction_update::Message::PartialToolCall(
|
||||
pb::PartialToolCallUpdate {
|
||||
call_id: call.call_id.clone(),
|
||||
tool_call: Some(pb::ToolCall {
|
||||
hook_additional_contexts: Vec::new(),
|
||||
tool_call_id: Some(call.call_id.clone()),
|
||||
started_at_ms: None,
|
||||
completed_at_ms: None,
|
||||
tool: Some(pb::tool_call::Tool::TaskToolCall(pb::TaskToolCall {
|
||||
args: Some(pb::TaskArgs {
|
||||
description: description.into(),
|
||||
prompt: prompt.into(),
|
||||
subagent_type: Some(subagent_type(subagent)),
|
||||
model: (!model.is_empty()).then(|| model.into()),
|
||||
resume: (!resume.is_empty()).then(|| resume.into()),
|
||||
agent_id: None,
|
||||
attachments: Vec::new(),
|
||||
mode: 0,
|
||||
responding_to_message_ids: Vec::new(),
|
||||
environment: execution_environment(
|
||||
(!environment.is_empty()).then_some(environment),
|
||||
),
|
||||
machine: None,
|
||||
}),
|
||||
result: None,
|
||||
..Default::default()
|
||||
})),
|
||||
}),
|
||||
args_text_delta: String::new(),
|
||||
model_call_id: call.model_call_id.clone(),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub(crate) fn create_plan_partial(
|
||||
call: &ToolCall,
|
||||
name: &str,
|
||||
|
||||
@@ -20,6 +20,7 @@ enum Presentation {
|
||||
DynamicMcp(pb::McpToolDefinition),
|
||||
Edit(EditProjection),
|
||||
CreatePlan(CreatePlanProjection),
|
||||
Task(TaskProjection),
|
||||
}
|
||||
|
||||
struct EditProjection {
|
||||
@@ -38,6 +39,17 @@ struct CreatePlanProjection {
|
||||
overview: String,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct TaskProjection {
|
||||
fields: JsonStringFields,
|
||||
description: String,
|
||||
prompt: String,
|
||||
subagent_type: String,
|
||||
model: String,
|
||||
resume: String,
|
||||
environment: String,
|
||||
}
|
||||
|
||||
impl ToolCallStream {
|
||||
pub fn new(name: &str, dynamic_mcp: Option<&pb::McpToolDefinition>) -> Self {
|
||||
let presentation = match dynamic_mcp {
|
||||
@@ -49,6 +61,7 @@ impl ToolCallStream {
|
||||
Presentation::Edit(EditProjection::new("target_notebook", "new_string"))
|
||||
}
|
||||
"createplan" => Presentation::CreatePlan(CreatePlanProjection::default()),
|
||||
"task" => Presentation::Task(TaskProjection::default()),
|
||||
_ => Presentation::Plain,
|
||||
},
|
||||
};
|
||||
@@ -73,10 +86,48 @@ impl ToolCallStream {
|
||||
Ok(messages)
|
||||
}
|
||||
Presentation::CreatePlan(plan) => plan.project(call, raw_delta),
|
||||
Presentation::Task(task) => task.project(call, raw_delta),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TaskProjection {
|
||||
fn project(&mut self, call: &ToolCall, raw_delta: &str) -> Result<Vec<pb::AgentServerMessage>> {
|
||||
let mut description_completed = false;
|
||||
for event in self.fields.push(raw_delta)? {
|
||||
match event {
|
||||
StringFieldEvent::Delta { name, text } => match name.as_str() {
|
||||
"description" => self.description.push_str(&text),
|
||||
"prompt" => self.prompt.push_str(&text),
|
||||
"subagent_type" => self.subagent_type.push_str(&text),
|
||||
"model" => self.model.push_str(&text),
|
||||
"resume" => self.resume.push_str(&text),
|
||||
"environment" => self.environment.push_str(&text),
|
||||
_ => {}
|
||||
},
|
||||
StringFieldEvent::End { name } if name == "description" => {
|
||||
description_completed = true
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
Ok(description_completed
|
||||
.then(|| {
|
||||
interaction::task_partial(
|
||||
call,
|
||||
&self.description,
|
||||
&self.prompt,
|
||||
&self.subagent_type,
|
||||
&self.model,
|
||||
&self.resume,
|
||||
&self.environment,
|
||||
)
|
||||
})
|
||||
.into_iter()
|
||||
.collect())
|
||||
}
|
||||
}
|
||||
|
||||
impl CreatePlanProjection {
|
||||
fn project(&mut self, call: &ToolCall, raw_delta: &str) -> Result<Vec<pb::AgentServerMessage>> {
|
||||
let mut completed_field = false;
|
||||
@@ -184,3 +235,53 @@ fn normalized(value: &str) -> String {
|
||||
.flat_map(char::to_lowercase)
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn task_call(arguments_text: &str) -> ToolCall {
|
||||
ToolCall {
|
||||
index: 0,
|
||||
call_id: "task-1".into(),
|
||||
model_call_id: "model-1".into(),
|
||||
name: "Task".into(),
|
||||
arguments_text: arguments_text.into(),
|
||||
arguments: serde_json::Value::Null,
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn task_description_projects_a_visible_partial_card_before_execution() {
|
||||
let mut stream = ToolCallStream::new("Task", None);
|
||||
let first = r#"{"description":"Review K10"#;
|
||||
assert!(stream
|
||||
.arguments_delta(&task_call(first), first)
|
||||
.unwrap()
|
||||
.is_empty());
|
||||
|
||||
let closing = "\",";
|
||||
let messages = stream
|
||||
.arguments_delta(&task_call(&format!("{first}{closing}")), closing)
|
||||
.unwrap();
|
||||
assert_eq!(messages.len(), 1);
|
||||
let Some(pb::agent_server_message::Message::InteractionUpdate(update)) =
|
||||
messages[0].message.as_ref()
|
||||
else {
|
||||
panic!("expected interaction update")
|
||||
};
|
||||
let Some(pb::interaction_update::Message::PartialToolCall(partial)) =
|
||||
update.message.as_ref()
|
||||
else {
|
||||
panic!("expected partial tool call")
|
||||
};
|
||||
let tool_call = partial.tool_call.as_ref().expect("expected tool call");
|
||||
assert_eq!(tool_call.started_at_ms, None);
|
||||
let Some(pb::tool_call::Tool::TaskToolCall(task)) = tool_call.tool.as_ref() else {
|
||||
panic!("expected Task tool call")
|
||||
};
|
||||
let args = task.args.as_ref().expect("expected partial Task args");
|
||||
assert_eq!(args.description, "Review K10");
|
||||
assert_eq!(args.prompt, "");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user