feat: add AskUserQuestion + Task tool specs and stubs

Port 7 missing tool definitions from upstream parity audit:
- AskUserQuestionTool: ask user a question with optional choices
- TaskCreate: create background sub-agent task
- TaskGet: get task status by ID
- TaskList: list all background tasks
- TaskStop: stop a running task
- TaskUpdate: send message to a running task
- TaskOutput: retrieve task output

All tools have full ToolSpec schemas registered in mvp_tool_specs()
and stub execute functions wired into execute_tool(). Stubs return
structured JSON responses; real sub-agent runtime integration is the
next step.

Closes parity gap: 21 -> 28 tools (upstream has 40).
fmt/clippy/tests all green.
This commit is contained in:
Jobdori
2026-04-03 07:39:21 +09:00
parent 06151c57f3
commit 64f4ed0ad8

View File

@@ -564,6 +564,100 @@ pub fn mvp_tool_specs() -> Vec<ToolSpec> {
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "AskUserQuestion",
description: "Ask the user a question and wait for their response.",
input_schema: json!({
"type": "object",
"properties": {
"question": { "type": "string" },
"options": {
"type": "array",
"items": { "type": "string" }
}
},
"required": ["question"],
"additionalProperties": false
}),
required_permission: PermissionMode::ReadOnly,
},
ToolSpec {
name: "TaskCreate",
description: "Create a background task that runs in a separate subprocess.",
input_schema: json!({
"type": "object",
"properties": {
"prompt": { "type": "string" },
"description": { "type": "string" }
},
"required": ["prompt"],
"additionalProperties": false
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "TaskGet",
description: "Get the status and details of a background task by ID.",
input_schema: json!({
"type": "object",
"properties": {
"task_id": { "type": "string" }
},
"required": ["task_id"],
"additionalProperties": false
}),
required_permission: PermissionMode::ReadOnly,
},
ToolSpec {
name: "TaskList",
description: "List all background tasks and their current status.",
input_schema: json!({
"type": "object",
"properties": {},
"additionalProperties": false
}),
required_permission: PermissionMode::ReadOnly,
},
ToolSpec {
name: "TaskStop",
description: "Stop a running background task by ID.",
input_schema: json!({
"type": "object",
"properties": {
"task_id": { "type": "string" }
},
"required": ["task_id"],
"additionalProperties": false
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "TaskUpdate",
description: "Send a message or update to a running background task.",
input_schema: json!({
"type": "object",
"properties": {
"task_id": { "type": "string" },
"message": { "type": "string" }
},
"required": ["task_id", "message"],
"additionalProperties": false
}),
required_permission: PermissionMode::DangerFullAccess,
},
ToolSpec {
name: "TaskOutput",
description: "Retrieve the output produced by a background task.",
input_schema: json!({
"type": "object",
"properties": {
"task_id": { "type": "string" }
},
"required": ["task_id"],
"additionalProperties": false
}),
required_permission: PermissionMode::ReadOnly,
},
]
}
@@ -592,10 +686,90 @@ pub fn execute_tool(name: &str, input: &Value) -> Result<String, String> {
}
"REPL" => from_value::<ReplInput>(input).and_then(run_repl),
"PowerShell" => from_value::<PowerShellInput>(input).and_then(run_powershell),
"AskUserQuestion" => {
from_value::<AskUserQuestionInput>(input).and_then(run_ask_user_question)
}
"TaskCreate" => from_value::<TaskCreateInput>(input).and_then(run_task_create),
"TaskGet" => from_value::<TaskIdInput>(input).and_then(run_task_get),
"TaskList" => run_task_list(input.clone()),
"TaskStop" => from_value::<TaskIdInput>(input).and_then(run_task_stop),
"TaskUpdate" => from_value::<TaskUpdateInput>(input).and_then(run_task_update),
"TaskOutput" => from_value::<TaskIdInput>(input).and_then(run_task_output),
_ => Err(format!("unsupported tool: {name}")),
}
}
#[allow(clippy::needless_pass_by_value)]
fn run_ask_user_question(input: AskUserQuestionInput) -> Result<String, String> {
let mut result = json!({
"question": input.question,
"status": "pending",
"message": "Waiting for user response"
});
if let Some(options) = &input.options {
result["options"] = json!(options);
}
to_pretty_json(result)
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_create(input: TaskCreateInput) -> Result<String, String> {
let secs = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let task_id = format!("task_{secs:08x}");
to_pretty_json(json!({
"task_id": task_id,
"status": "created",
"prompt": input.prompt,
"description": input.description
}))
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_get(input: TaskIdInput) -> Result<String, String> {
to_pretty_json(json!({
"task_id": input.task_id,
"status": "unknown",
"message": "Task runtime not yet implemented"
}))
}
fn run_task_list(_input: Value) -> Result<String, String> {
to_pretty_json(json!({
"tasks": [],
"message": "No tasks found"
}))
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_stop(input: TaskIdInput) -> Result<String, String> {
to_pretty_json(json!({
"task_id": input.task_id,
"status": "stopped",
"message": "Task stop requested"
}))
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_update(input: TaskUpdateInput) -> Result<String, String> {
to_pretty_json(json!({
"task_id": input.task_id,
"status": "updated",
"message": input.message
}))
}
#[allow(clippy::needless_pass_by_value)]
fn run_task_output(input: TaskIdInput) -> Result<String, String> {
to_pretty_json(json!({
"task_id": input.task_id,
"output": "",
"message": "No output available"
}))
}
fn from_value<T: for<'de> Deserialize<'de>>(input: &Value) -> Result<T, String> {
serde_json::from_value(input.clone()).map_err(|error| error.to_string())
}
@@ -875,6 +1049,31 @@ struct PowerShellInput {
run_in_background: Option<bool>,
}
#[derive(Debug, Deserialize)]
struct AskUserQuestionInput {
question: String,
#[serde(default)]
options: Option<Vec<String>>,
}
#[derive(Debug, Deserialize)]
struct TaskCreateInput {
prompt: String,
#[serde(default)]
description: Option<String>,
}
#[derive(Debug, Deserialize)]
struct TaskIdInput {
task_id: String,
}
#[derive(Debug, Deserialize)]
struct TaskUpdateInput {
task_id: String,
message: String,
}
#[derive(Debug, Serialize)]
struct WebFetchOutput {
bytes: usize,