/**
* @fileoverview Task management utilities for orchestrator tools providing
* high-level operations for creating, updating, and managing tasks
* @module handlers/tools/orchestrator/utils/task
*/
import { v4 as uuidv4 } from "uuid";
import { TaskStore } from "../../../services/task-store.js";
import { logger } from "../../../utils/logger.js";
import type {
Task,
TaskStatus,
AITool,
createTaskId,
UpdateTaskParams,
TaskLogEntry,
} from "../../../types/task.js";
import { TASK_STATUS } from "../../../constants/task-status.js";
/**
* Parameters for creating a new task
*/
export interface TaskCreationParams {
title?: string;
description: string;
tool: AITool;
projectPath: string;
}
/**
* Task report structure
*/
export interface TaskReport {
task: Task;
duration: string;
logCount: number;
summary: string;
}
/**
* High-level utilities for task management
*/
export class TaskOperations {
public readonly taskStore: TaskStore;
constructor(taskStore?: TaskStore) {
this.taskStore = taskStore || TaskStore.getInstance();
}
/**
* Creates a new task
*
* @param params - Task creation parameters
* @param sessionId - Optional session ID
* @returns Created task object
*/
async createTask(params: TaskCreationParams, sessionId?: string): Promise<Task> {
const taskId = uuidv4();
const now = new Date().toISOString();
const task: Task = {
id: taskId as ReturnType<typeof createTaskId>,
description: params.description,
tool: params.tool,
status: TASK_STATUS.PENDING,
created_at: now,
updated_at: now,
logs: [],
};
await this.taskStore.createTask(task, sessionId);
logger.info("Task created", { taskId, tool: params.tool });
const logEntry: TaskLogEntry = {
timestamp: new Date().toISOString(),
level: 'info',
type: 'system',
message: `Task created with ${params.tool} tool`,
metadata: {
tool: params.tool,
taskId,
}
};
await this.taskStore.addLog(taskId, logEntry, sessionId);
return task;
}
/**
* Updates task status with validation
*
* @param taskId - Task ID to update
* @param newStatus - New task status
* @param sessionId - Optional session ID
* @param metadata - Optional metadata including error, result, etc
* @returns Updated task or null if not found
*/
async updateTaskStatus(
taskId: string,
newStatus: TaskStatus,
sessionId?: string,
metadata?: {
error?: string;
result?: unknown;
completedAt?: string;
},
): Promise<Task | null> {
const task = await this.taskStore.getTask(taskId);
if (!task) {
logger.warn("Task not found for status update", { taskId });
return null;
}
if (!this.isValidStatusTransition(task.status, newStatus)) {
logger.warn("Invalid status transition", {
taskId,
from: task.status,
to: newStatus,
});
throw new Error(`Cannot transition from ${task.status} to ${newStatus}`);
}
const updates: UpdateTaskParams = {
status: newStatus,
};
switch (newStatus) {
case TASK_STATUS.IN_PROGRESS:
if (!task.started_at) {
updates.started_at = new Date().toISOString();
}
break;
case TASK_STATUS.WAITING:
// Store the result for waiting tasks
if (metadata?.result !== undefined && metadata.result !== null) {
// Ensure result has the correct structure
const result = metadata.result as any;
if (result && typeof result === 'object' && 'output' in result && 'success' in result) {
updates.result = result;
}
}
break;
case TASK_STATUS.COMPLETED:
case TASK_STATUS.FAILED:
case TASK_STATUS.CANCELLED:
if (!task.completed_at) {
updates.completed_at = metadata?.completedAt || new Date().toISOString();
}
if (metadata?.error) {
updates.error = metadata.error;
}
if (metadata?.result !== undefined && metadata.result !== null) {
// Ensure result has the correct structure
const result = metadata.result as any;
if (result && typeof result === 'object' && 'output' in result && 'success' in result) {
updates.result = result;
}
}
break;
}
await this.taskStore.updateTask(taskId, updates, sessionId);
const logEntry: TaskLogEntry = {
timestamp: new Date().toISOString(),
level: 'info',
type: 'system',
message: `Status: ${newStatus}`,
metadata: {
previousStatus: task.status,
newStatus,
taskId,
}
};
await this.taskStore.addLog(taskId, logEntry, sessionId);
return { ...task, ...updates };
}
/**
* Checks if a status transition is valid
*
* @param from - Current status
* @param to - Target status
* @returns True if transition is valid
*/
private isValidStatusTransition(from: TaskStatus, to: TaskStatus): boolean {
const validTransitions: Record<TaskStatus, TaskStatus[]> = {
[TASK_STATUS.PENDING]: [TASK_STATUS.IN_PROGRESS, TASK_STATUS.CANCELLED],
[TASK_STATUS.IN_PROGRESS]: [TASK_STATUS.WAITING, TASK_STATUS.COMPLETED, TASK_STATUS.FAILED, TASK_STATUS.CANCELLED],
[TASK_STATUS.WAITING]: [TASK_STATUS.COMPLETED],
[TASK_STATUS.COMPLETED]: [],
[TASK_STATUS.FAILED]: [],
[TASK_STATUS.CANCELLED]: [],
};
return validTransitions[from]?.includes(to) || false;
}
/**
* Generates a task report
*
* @param taskId - Task ID to report on
* @param format - Report format (markdown, json, or summary)
* @returns Formatted report string
*/
async generateTaskReport(
taskId: string,
format: "markdown" | "json" | "summary" = "markdown",
): Promise<string> {
const task = await this.taskStore.getTask(taskId);
if (!task) {
throw new Error(`Task not found: ${taskId}`);
}
const logs = await this.taskStore.getLogs(taskId);
const duration = this.calculateDuration(task);
switch (format) {
case "json":
return JSON.stringify(
{
task,
logs,
duration,
metrics: this.calculateTaskMetrics(task, logs),
},
null,
2,
);
case "summary":
return this.generateSummaryReport(task, logs, duration);
case "markdown":
default:
return this.generateMarkdownReport(task, logs, duration);
}
}
/**
* Generates a markdown report
*
* @param task - Task object
* @param logs - Task log entries
* @param duration - Formatted duration string
* @returns Markdown formatted report
*/
private generateMarkdownReport(task: Task, logs: TaskLogEntry[], duration: string): string {
const status =
task.status === TASK_STATUS.COMPLETED
? "✅"
: task.status === TASK_STATUS.WAITING
? "⏳✅"
: task.status === TASK_STATUS.FAILED
? "❌"
: task.status === TASK_STATUS.CANCELLED
? "⏹️"
: task.status === TASK_STATUS.IN_PROGRESS
? "🔄"
: "⏳";
let report = `# Task Report\n\n`;
report += `**ID:** ${task.id}\n`;
report += `**Status:** ${status} ${task.status}\n`;
report += `**Tool:** ${task.tool === "CLAUDECODE" ? "Claude Code" : "Gemini CLI"}\n`;
report += `**Duration:** ${duration}\n`;
report += `**Created:** ${new Date(task.created_at).toLocaleString()}\n`;
if (task.started_at) {
report += `**Started:** ${new Date(task.started_at).toLocaleString()}\n`;
}
if (task.completed_at) {
report += `**Completed:** ${new Date(task.completed_at).toLocaleString()}\n`;
}
report += `\n## Description\n\n${task.description}\n`;
if (task.error) {
report += `\n## Error\n\n\`\`\`\n${task.error}\n\`\`\`\n`;
}
if (logs.length > 0) {
report += `\n## Execution Logs (${logs.length} entries)\n\n`;
report += "```\n";
report += logs.slice(-50).map(log => {
const prefix = log.prefix ? `[${log.prefix}]` : '';
const level = log.level !== 'info' ? `[${log.level.toUpperCase()}]` : '';
return `[${log.timestamp}] ${level}${prefix} ${log.message}`;
}).join("\n");
report += "\n```\n";
}
return report;
}
/**
* Generates a summary report
*
* @param task - Task object
* @param logs - Task log entries
* @param duration - Formatted duration string
* @returns Summary text report
*/
private generateSummaryReport(task: Task, logs: TaskLogEntry[], duration: string): string {
const errorCount = logs.filter((log) => log.level === 'error').length;
const warningCount = logs.filter((log) => log.level === 'warn').length;
return [
`Task ID: ${task.id}`,
`Status: ${task.status}`,
`Duration: ${duration}`,
`Logs: ${logs.length} entries (${errorCount} errors, ${warningCount} warnings)`,
task.error ? `Error: ${task.error.substring(0, 100)}...` : "",
]
.filter(Boolean)
.join("\n");
}
/**
* Calculates task duration
*
* @param task - Task object
* @returns Human-readable duration string
*/
private calculateDuration(task: Task): string {
if (!task.started_at) {
return "Not started";
}
const start = new Date(task.started_at).getTime();
const end = task.completed_at ? new Date(task.completed_at).getTime() : Date.now();
const seconds = Math.floor((end - start) / 1000);
if (seconds < 60) {
return `${seconds}s`;
}
const minutes = Math.floor(seconds / 60);
const remainingSeconds = seconds % 60;
if (minutes < 60) {
return `${minutes}m ${remainingSeconds}s`;
}
const hours = Math.floor(minutes / 60);
const remainingMinutes = minutes % 60;
return `${hours}h ${remainingMinutes}m ${remainingSeconds}s`;
}
/**
* Calculates task metrics
*
* @param task - Task object
* @param logs - Task log entries
* @returns Task metrics object
*/
private calculateTaskMetrics(task: Task, logs: TaskLogEntry[]): Record<string, unknown> {
return {
totalLogs: logs.length,
errorCount: logs.filter((log) => log.level === 'error').length,
warningCount: logs.filter((log) => log.level === 'warn').length,
toolCalls: logs.filter((log) => log.type === 'tool').length,
agentMessages: logs.filter((log) => log.type === 'agent').length,
systemEvents: logs.filter((log) => log.type === 'system').length,
outputLines: logs.filter((log) => log.type === 'output').length,
status: task.status,
};
}
/**
* Updates a task
*
* @param taskId - Task ID to update
* @param updates - Partial task updates
* @param sessionId - Optional session ID
* @returns Updated task or null
*/
async updateTask(
taskId: string,
updates: Partial<Task>,
sessionId?: string,
): Promise<Task | null> {
return this.taskStore.updateTask(taskId, updates, sessionId);
}
/**
* Adds a log entry
*
* @param taskId - Task ID to add log to
* @param log - Log message or entry object
* @param sessionId - Optional session ID
*/
async addTaskLog(taskId: string, log: string | TaskLogEntry, sessionId?: string): Promise<void> {
await this.taskStore.addLog(taskId, log, sessionId);
}
/**
* Gets task statistics
*
* @returns Task statistics including counts by status and tool
*/
async getTaskStatistics(): Promise<{
total: number;
byStatus: Record<TaskStatus, number>;
byTool: Record<string, number>;
averageDuration: number;
successRate: number;
}> {
const tasks = await this.taskStore.getTasks();
const byStatus = tasks.reduce(
(acc: Record<TaskStatus, number>, task: Task) => {
acc[task.status] = (acc[task.status] || 0) + 1;
return acc;
},
{} as Record<TaskStatus, number>,
);
const byTool = tasks.reduce(
(acc: Record<string, number>, task: Task) => {
acc[task.tool] = (acc[task.tool] || 0) + 1;
return acc;
},
{} as Record<string, number>,
);
const completedTasks = tasks.filter((t: Task) => t.status === TASK_STATUS.COMPLETED);
const totalDuration = completedTasks.reduce((sum: number, task: Task) => {
if (task.started_at && task.completed_at) {
const start = new Date(task.started_at).getTime();
const end = new Date(task.completed_at).getTime();
return sum + Math.floor((end - start) / 1000);
}
return sum;
}, 0);
const successRate = tasks.length > 0 ? (completedTasks.length / tasks.length) * 100 : 0;
return {
total: tasks.length,
byStatus,
byTool,
averageDuration: completedTasks.length > 0 ? totalDuration / completedTasks.length : 0,
successRate,
};
}
}
/**
* Singleton instance of task operations - initialized lazily to avoid circular dependencies
*/
let _taskOperations: TaskOperations | null = null;
/**
* Export singleton with lazy initialization
*/
export const taskOperations = {
get instance(): TaskOperations {
if (!_taskOperations) {
_taskOperations = new TaskOperations();
}
return _taskOperations;
},
// Direct property access
get taskStore() { return this.instance.taskStore; },
// Method delegations
createTask: (...args: Parameters<TaskOperations['createTask']>) => taskOperations.instance.createTask(...args),
updateTaskStatus: (...args: Parameters<TaskOperations['updateTaskStatus']>) => taskOperations.instance.updateTaskStatus(...args),
updateTask: (...args: Parameters<TaskOperations['updateTask']>) => taskOperations.instance.updateTask(...args),
addTaskLog: (...args: Parameters<TaskOperations['addTaskLog']>) => taskOperations.instance.addTaskLog(...args),
generateTaskReport: (...args: Parameters<TaskOperations['generateTaskReport']>) => taskOperations.instance.generateTaskReport(...args),
getTaskStatistics: (...args: Parameters<TaskOperations['getTaskStatistics']>) => taskOperations.instance.getTaskStatistics(...args),
// Delegate to taskStore
getTask: (taskId: string) => taskOperations.instance.taskStore.getTask(taskId),
getAllTasks: () => taskOperations.instance.taskStore.getAllTasks(),
deleteTask: (taskId: string) => taskOperations.instance.taskStore.deleteTask(taskId),
};