Files
homeclaw/src/tools/task-control.ts
T
2026-04-29 11:45:59 +09:00

247 lines
8.1 KiB
TypeScript

/**
* task-control.ts - Task management tool
*
* Exposes TaskStore operations as a tool so agents can:
* - List tasks (with filtering)
* - Get specific task details
* - Create new tasks
* - Update task status/progress
* - Cancel tasks
*
* Used by BOOT.md and automation workflows.
*/
import { ToolResult } from '../types.js';
import {
listTasks,
createTask,
loadTask,
saveTask,
updateTaskStatus,
appendJournal,
deleteTask,
type TaskRecord,
type TaskStatus,
} from '../gateway/task-store.js';
const VALID_STATUSES: TaskStatus[] = [
'queued', 'running', 'paused', 'stalled', 'needs_assistance',
'complete', 'failed', 'waiting_subagent',
];
export const taskControlTool = {
name: 'task_control',
description: 'Manage workspace tasks: list, get, create, update, cancel, delete',
schema: {
action: 'Action: list, get, create, update, cancel, or delete',
task_id: 'Task ID for get/update/cancel/delete actions',
goal: 'Task goal/description for create action',
status: 'Filter by status for list action (e.g. "pending", "running", "done", "failed")',
include_all_sessions: 'Include tasks from all sessions (for list)',
limit: 'Max results for list action (default 20)',
new_status: 'New status for update action',
journal_entry: 'Journal entry to append for update action',
},
jsonSchema: {
type: 'object',
properties: {
action: {
type: 'string',
enum: ['list', 'get', 'create', 'update', 'cancel', 'delete'],
description: 'Action to perform',
},
task_id: {
type: 'string',
description: 'Task ID for get/update/cancel/delete actions',
},
goal: {
type: 'string',
description: 'Task goal/description for create action',
},
status: {
type: 'string',
description: 'Filter by status for list action',
},
include_all_sessions: {
type: 'boolean',
description: 'Include tasks from all sessions (default false)',
},
limit: {
type: 'number',
description: 'Max results for list action (default 20)',
},
new_status: {
type: 'string',
description: 'New status for update action',
},
journal_entry: {
type: 'string',
description: 'Journal entry to append for update action',
},
},
required: ['action'],
additionalProperties: true,
},
execute: async (args: any): Promise<ToolResult> => {
try {
const {
action,
task_id,
goal,
status,
include_all_sessions,
limit,
new_status,
journal_entry,
} = args || {};
if (!action) {
return {
success: false,
error: 'action is required. Valid actions: list, get, create, update, cancel, delete',
};
}
const normalizedAction = String(action).toLowerCase().trim();
// LIST tasks
if (normalizedAction === 'list') {
try {
const allTasks = listTasks();
let filtered = allTasks;
if (status) {
const statusStr = String(status).toLowerCase().trim();
filtered = filtered.filter(t => String(t.status || '').toLowerCase() === statusStr);
}
const maxResults = Math.max(1, Math.min(limit || 20, 100));
const results = filtered.slice(0, maxResults);
return {
success: true,
stdout: `Listed ${results.length} task(s)`,
data: {
count: results.length,
total_available: filtered.length,
tasks: results.map((t: TaskRecord) => ({
id: t.id,
title: t.title,
prompt: t.prompt,
status: t.status,
startedAt: t.startedAt,
lastProgressAt: t.lastProgressAt,
stepCount: t.journal?.length || 0,
})),
},
};
} catch (err: any) {
return { success: false, error: `Failed to list tasks: ${err?.message || err}` };
}
}
// GET task
if (normalizedAction === 'get') {
if (!task_id) return { success: false, error: 'task_id is required for get action' };
try {
const task = loadTask(String(task_id));
if (!task) return { success: false, error: `Task not found: ${task_id}` };
return {
success: true,
stdout: `Loaded task: ${task.title}`,
data: task,
};
} catch (err: any) {
return { success: false, error: `Failed to get task: ${err?.message || err}` };
}
}
// CREATE task
if (normalizedAction === 'create') {
if (!goal) return { success: false, error: 'goal is required for create action' };
try {
const task = createTask({
title: String(goal).slice(0, 120),
prompt: String(goal),
sessionId: 'tool-created',
channel: 'web',
plan: [{ index: 0, description: String(goal), status: 'pending' }],
});
return {
success: true,
stdout: `Created task: ${task.id}`,
data: { id: task.id, title: task.title, status: task.status },
};
} catch (err: any) {
return { success: false, error: `Failed to create task: ${err?.message || err}` };
}
}
// UPDATE task
if (normalizedAction === 'update') {
if (!task_id) return { success: false, error: 'task_id is required for update action' };
try {
const task = loadTask(String(task_id));
if (!task) return { success: false, error: `Task not found: ${task_id}` };
if (new_status) {
const s = String(new_status) as TaskStatus;
if (!VALID_STATUSES.includes(s)) {
return { success: false, error: `Invalid status "${new_status}". Valid: ${VALID_STATUSES.join(', ')}` };
}
task.status = s;
task.lastProgressAt = Date.now();
}
if (journal_entry) {
appendJournal(task.id, { type: 'status_push', content: String(journal_entry) });
}
saveTask(task);
return {
success: true,
stdout: `Updated task: ${task_id}`,
data: { id: task.id, status: task.status, journal_entries: task.journal?.length || 0 },
};
} catch (err: any) {
return { success: false, error: `Failed to update task: ${err?.message || err}` };
}
}
// CANCEL task
if (normalizedAction === 'cancel') {
if (!task_id) return { success: false, error: 'task_id is required for cancel action' };
try {
const task = loadTask(String(task_id));
if (!task) return { success: false, error: `Task not found: ${task_id}` };
updateTaskStatus(String(task_id), 'failed');
appendJournal(String(task_id), { type: 'status_push', content: 'Task cancelled by operator.' });
return { success: true, stdout: `Cancelled task: ${task_id}`, data: { id: task_id, status: 'failed' } };
} catch (err: any) {
return { success: false, error: `Failed to cancel task: ${err?.message || err}` };
}
}
// DELETE task
if (normalizedAction === 'delete') {
if (!task_id) return { success: false, error: 'task_id is required for delete action' };
try {
const task = loadTask(String(task_id));
if (!task) return { success: false, error: `Task not found: ${task_id}` };
deleteTask(String(task_id));
return { success: true, stdout: `Deleted task: ${task_id}`, data: { id: task_id } };
} catch (err: any) {
return { success: false, error: `Failed to delete task: ${err?.message || err}` };
}
}
return {
success: false,
error: `Unknown action: ${action}. Valid actions: list, get, create, update, cancel, delete`,
};
} catch (err: any) {
return { success: false, error: `task_control error: ${err?.message || err}` };
}
},
};