use serde_json::Value;
use crate::store::{self, WorksetDefinition, WorksetItemDefinition};
use crate::tools::{require_str, ToolResult, ToolRuntime};
use crate::types::ToolDefinition;
pub fn define_definition() -> ToolDefinition {
use serde_json::json;
def(
"workset_define",
"Create or replace a durable high-level plan workset for this session.",
json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Short stable handle for this workset. This is what the user passes to /run <workset>, so prefer lowercase words separated by hyphens."
},
"goal": {
"type": "string",
"description": "Durable user-facing objective for the whole plan. Capture what should be true when the workset is complete, not the orchestrator's current focus."
},
"status": {
"type": "string",
"description": "Whole-plan state, such as planned, running, blocked, completed, or abandoned."
},
"summary": {
"type": "string",
"description": "Compact synopsis of the plan and its current state. Keep it short enough to scan in the worksets pane."
},
"verification_recipe": {
"type": "string",
"description": "Optional end-to-end validation recipe for the workset, such as tests or manual checks that prove the goal was met."
},
"items": {
"type": "array",
"description": "Ordered high-level plan items. Order should reflect dependencies and the natural execution sequence.",
"items": {
"type": "object",
"properties": {
"title": {
"type": "string",
"description": "Concise label for this item. Make it stable enough to reference from depends_on."
},
"scope": {
"type": "string",
"description": "Owned files, modules, product area, or system boundary for this item. Use this to prevent overlapping implementation ownership."
},
"description": {
"type": "string",
"description": "Concrete work to do for this item, including important constraints or context."
},
"role": {
"type": "string",
"description": "Intended mode for the item, such as research, implementation, verification, cleanup, or coordination."
},
"depends_on": {
"type": "array",
"description": "Prerequisite workset item titles or ids that should be satisfied before this item starts. Use an empty array when there are none.",
"items": { "type": "string" }
},
"acceptance": {
"type": "string",
"description": "Concrete condition that makes this item complete. Prefer observable outcomes over vague intent."
},
"notes": {
"type": "string",
"description": "Optional durable context, risks, discoveries, or execution notes for this item."
},
"status": {
"type": "string",
"description": "Per-item status, such as planned, in_progress, blocked, completed, or skipped. Defaults to planned when omitted."
}
},
"required": ["title", "scope", "description", "role", "depends_on", "acceptance"]
}
}
},
"required": ["id", "goal", "status", "summary", "items"]
}),
)
}
pub fn update_item_definition() -> ToolDefinition {
use serde_json::json;
def(
"workset_update_item",
"Update a single workset item's status and notes without replacing the entire workset. Use this to track execution progress as you complete work.",
json!({
"type": "object",
"properties": {
"id": {
"type": "string",
"description": "Workset id"
},
"title": {
"type": "string",
"description": "Title of the item to update (must match exactly)"
},
"status": {
"type": "string",
"description": "New status: planned, running, blocked, done"
},
"notes": {
"type": "string",
"description": "Key findings, results, or context to record"
}
},
"required": ["id", "title", "status"]
}),
)
}
pub fn read_definition() -> ToolDefinition {
use serde_json::json;
def(
"workset_read",
"Read the full structured definition of one workset in the current session.",
json!({
"type": "object",
"properties": {
"id": { "type": "string", "description": "Workset id." }
},
"required": ["id"]
}),
)
}
pub fn list_definition() -> ToolDefinition {
use serde_json::json;
def(
"workset_list",
"List persisted worksets in the current session.",
json!({
"type": "object",
"properties": {}
}),
)
}
pub async fn execute_define(args: Value, runtime: &ToolRuntime) -> ToolResult {
let session_id = match require_session(runtime) {
Ok(session_id) => session_id.to_string(),
Err(error) => return error,
};
let id = match require_str(&args, "id") {
Ok(id) => id,
Err(error) => return error,
};
let goal = match require_str(&args, "goal") {
Ok(goal) => goal,
Err(error) => return error,
};
let status = match require_str(&args, "status") {
Ok(status) => status,
Err(error) => return error,
};
let summary = match require_str(&args, "summary") {
Ok(summary) => summary,
Err(error) => return error,
};
let verification_recipe = match optional_string(&args, "verification_recipe") {
Ok(recipe) => recipe,
Err(error) => return error,
};
let items = match parse_items(args.get("items")) {
Ok(items) => items,
Err(error) => return error,
};
let definition = WorksetDefinition {
id: id.clone(),
goal,
status,
summary,
verification_recipe,
items,
};
let store_path = runtime.store_path.clone();
let sid = session_id.clone();
let items_len = definition.items.len();
match tokio::task::spawn_blocking(move || store::define_workset(&store_path, &sid, &definition))
.await
{
Ok(Ok(())) => ToolResult {
content: format!("Saved workset '{}' with {} item(s).", id, items_len),
is_error: false,
},
Ok(Err(error)) => ToolResult {
content: format!("Error saving workset '{}': {}", id, error),
is_error: true,
},
Err(join_error) => ToolResult {
content: format!("Internal error saving workset '{}': {}", id, join_error),
is_error: true,
},
}
}
pub async fn execute_read(args: Value, runtime: &ToolRuntime) -> ToolResult {
let session_id = match require_session(runtime) {
Ok(session_id) => session_id.to_string(),
Err(error) => return error,
};
let id = match require_str(&args, "id") {
Ok(id) => id,
Err(error) => return error,
};
let store_path = runtime.store_path.clone();
let sid = session_id.clone();
let wid = id.clone();
match tokio::task::spawn_blocking(move || store::read_workset(&store_path, &sid, &wid)).await {
Ok(Ok(Some(workset))) => ToolResult {
content: store::render_workset_document(&workset),
is_error: false,
},
Ok(Ok(None)) => ToolResult {
content: format!("Workset '{}' does not exist in this session.", id),
is_error: true,
},
Ok(Err(error)) => ToolResult {
content: format!("Error reading workset '{}': {}", id, error),
is_error: true,
},
Err(join_error) => ToolResult {
content: format!("Internal error reading workset '{}': {}", id, join_error),
is_error: true,
},
}
}
pub async fn execute_list(_args: Value, runtime: &ToolRuntime) -> ToolResult {
let session_id = match require_session(runtime) {
Ok(session_id) => session_id.to_string(),
Err(error) => return error,
};
let store_path = runtime.store_path.clone();
let sid = session_id.clone();
match tokio::task::spawn_blocking(move || store::list_worksets(&store_path, &sid)).await {
Ok(Ok(worksets)) => ToolResult {
content: store::render_workset_list(&worksets),
is_error: false,
},
Ok(Err(error)) => ToolResult {
content: format!("Error listing worksets: {}", error),
is_error: true,
},
Err(join_error) => ToolResult {
content: format!("Internal error listing worksets: {}", join_error),
is_error: true,
},
}
}
pub async fn execute_update_item(args: Value, runtime: &ToolRuntime) -> ToolResult {
let session_id = match require_session(runtime) {
Ok(session_id) => session_id.to_string(),
Err(error) => return error,
};
let id = match require_str(&args, "id") {
Ok(id) => id,
Err(error) => return error,
};
let title = match require_str(&args, "title") {
Ok(title) => title,
Err(error) => return error,
};
let status = match require_str(&args, "status") {
Ok(status) => status,
Err(error) => return error,
};
let notes = match optional_string(&args, "notes") {
Ok(notes) => notes,
Err(error) => return error,
};
match status.as_str() {
"planned" | "running" | "blocked" | "done" => {}
_ => {
return ToolResult {
content: format!(
"Error: invalid status '{}'. Must be one of: planned, running, blocked, done",
status
),
is_error: true,
};
}
}
let store_path = runtime.store_path.clone();
let sid = session_id.clone();
let wid = id.clone();
let t = title.clone();
let s = status.clone();
let n = notes.clone();
match tokio::task::spawn_blocking(move || {
store::update_workset_item(&store_path, &sid, &wid, &t, &s, n.as_deref())
})
.await
{
Ok(Ok(true)) => ToolResult {
content: format!(
"Updated item '{}' in workset '{}' to status '{}'",
title, id, status
),
is_error: false,
},
Ok(Ok(false)) => ToolResult {
content: format!("No item '{}' found in workset '{}'", title, id),
is_error: true,
},
Ok(Err(error)) => ToolResult {
content: format!(
"Error updating item '{}' in workset '{}': {}",
title, id, error
),
is_error: true,
},
Err(join_error) => ToolResult {
content: format!(
"Internal error updating item '{}' in workset '{}': {}",
title, id, join_error
),
is_error: true,
},
}
}
fn def(name: &str, description: &str, parameters: serde_json::Value) -> ToolDefinition {
ToolDefinition {
def_type: "function".to_string(),
function: crate::types::FunctionDef {
name: name.to_string(),
description: description.to_string(),
parameters,
},
}
}
fn require_session(runtime: &ToolRuntime) -> Result<&str, ToolResult> {
runtime.session_id.as_deref().ok_or_else(|| ToolResult {
content: "Error: workset tools require an active session".to_string(),
is_error: true,
})
}
fn optional_string(args: &Value, key: &str) -> Result<Option<String>, ToolResult> {
match args.get(key) {
None | Some(Value::Null) => Ok(None),
Some(Value::String(value)) => Ok(Some(value.clone())),
Some(_) => Err(ToolResult {
content: format!("Error: '{}' must be a string", key),
is_error: true,
}),
}
}
fn parse_items(value: Option<&Value>) -> Result<Vec<WorksetItemDefinition>, ToolResult> {
let Some(value) = value else {
return Err(ToolResult {
content: "Error: 'items' is required".to_string(),
is_error: true,
});
};
let Some(items) = value.as_array() else {
return Err(ToolResult {
content: "Error: 'items' must be an array".to_string(),
is_error: true,
});
};
let mut parsed = Vec::with_capacity(items.len());
for item in items {
let title = match require_item_str(item, "title") {
Ok(value) => value,
Err(error) => return Err(error),
};
let scope = match require_item_str(item, "scope") {
Ok(value) => value,
Err(error) => return Err(error),
};
let description = match require_item_str(item, "description") {
Ok(value) => value,
Err(error) => return Err(error),
};
let role = match require_item_str(item, "role") {
Ok(value) => value,
Err(error) => return Err(error),
};
let depends_on = match require_string_array(item, "depends_on") {
Ok(value) => value,
Err(error) => return Err(error),
};
let acceptance = match require_item_str(item, "acceptance") {
Ok(value) => value,
Err(error) => return Err(error),
};
let notes = match optional_string(item, "notes") {
Ok(value) => value,
Err(error) => return Err(error),
};
let status = match optional_string(item, "status") {
Ok(value) => value,
Err(error) => return Err(error),
};
parsed.push(WorksetItemDefinition {
title,
scope,
description,
role,
depends_on,
acceptance,
notes,
status,
});
}
Ok(parsed)
}
fn require_item_str(value: &Value, key: &str) -> Result<String, ToolResult> {
value
.get(key)
.and_then(Value::as_str)
.map(ToString::to_string)
.ok_or_else(|| ToolResult {
content: format!("Error: workset item '{}' is required", key),
is_error: true,
})
}
fn require_string_array(value: &Value, key: &str) -> Result<Vec<String>, ToolResult> {
let Some(value) = value.get(key) else {
return Err(ToolResult {
content: format!("Error: '{}' is required", key),
is_error: true,
});
};
let Some(items) = value.as_array() else {
return Err(ToolResult {
content: format!("Error: '{}' must be an array of strings", key),
is_error: true,
});
};
let mut parsed = Vec::with_capacity(items.len());
for item in items {
let Some(value) = item.as_str() else {
return Err(ToolResult {
content: format!("Error: '{}' must be an array of strings", key),
is_error: true,
});
};
parsed.push(value.to_string());
}
Ok(parsed)
}