import { TaskStatus, isTerminalStatus } from './task-state.js'; import type { PendingQuestion, TaskTypeName } from './task-state.js'; import type { TaskHandle, TaskResult, SessionMetrics } from './task-handle.js'; import type { TaskManager } from './task-manager.js'; /** * Factory function that creates a {@link TaskHandle} backed by a * {@link TaskManager}. * * Every method delegates to the manager — the handle is a thin, * task-scoped facade that providers interact with. */ export function createTaskHandle( manager: TaskManager, taskId: string, ): TaskHandle { const handle: TaskHandle = { taskId, // -- State transitions -------------------------------------------------- markRunning(sessionId?: string): void { const updates: Record = { status: TaskStatus.RUNNING, startedAt: new Date().toISOString(), }; if (sessionId !== undefined) { updates['sessionId'] = sessionId; } manager.updateTask(taskId, updates); }, markCompleted(result?: TaskResult, metrics?: SessionMetrics): void { const updates: Record = { status: TaskStatus.COMPLETED, }; if (result !== undefined) { updates['result'] = result; } if (metrics !== undefined) { // Store metrics as part of the result metadata // TaskState.result is typed as `unknown` — safe to assign if (result !== undefined) { updates['result'] = { ...result, _metrics: metrics }; } else { updates['result'] = { _metrics: metrics }; } } manager.updateTask(taskId, updates); }, markFailed(error: string, exitCode?: number): void { const updates: Record = { status: TaskStatus.FAILED, error, }; if (exitCode !== undefined) { updates['exitCode'] = exitCode; } manager.updateTask(taskId, updates); }, markCancelled(reason: string): void { manager.updateTask(taskId, { status: TaskStatus.CANCELLED, error: reason, }); // Fire abort if a controller is registered const controller = manager.getAbortController(taskId); if (controller) { controller.abort(reason); manager.fireAbortListeners(taskId); } }, markInputRequired(): void { manager.updateTask(taskId, { status: TaskStatus.WAITING_ANSWER, }); }, markRateLimited(reason: string, retryAfterMs?: number): void { const updates: Record = { status: TaskStatus.RATE_LIMITED, error: reason, }; if (retryAfterMs !== undefined) { updates['keepAlive'] = retryAfterMs; } manager.updateTask(taskId, updates); }, // -- Output ------------------------------------------------------------- writeOutput(line: string): void { manager.appendOutput(taskId, line); }, writeOutputFileOnly(line: string): void { manager.appendOutputFileOnly(taskId, line); }, // -- Lifecycle ---------------------------------------------------------- registerAbort(controller: AbortController): void { manager.registerAbort(taskId, controller); }, unregisterAbort(): void { manager.unregisterAbort(taskId); }, isTerminal(): boolean { const task = manager.getTask(taskId); if (!task) return true; // Treat missing task as terminal return isTerminalStatus(task.status); }, isAlive(): boolean { return !handle.isTerminal(); }, onAborted(cb: () => void): () => void { return manager.onAborted(taskId, cb); }, // -- Pending question queue --------------------------------------------- queuePendingQuestion(q: PendingQuestion): void { manager.queuePendingQuestion(taskId, q); }, dequeuePendingQuestion(): PendingQuestion | undefined { return manager.dequeuePendingQuestion(taskId); }, getPendingQuestions(): readonly PendingQuestion[] { return manager.getPendingQuestions(taskId); }, // -- Read-only accessors ------------------------------------------------ getPrompt(): string { const task = manager.getTask(taskId); return task?.prompt ?? ''; }, getCwd(): string { const task = manager.getTask(taskId); return task?.cwd ?? ''; }, getTimeout(): number { const task = manager.getTask(taskId); return task?.timeoutMs ?? 0; }, getModel(): string | undefined { const task = manager.getTask(taskId); return task?.model; }, getTaskType(): TaskTypeName { const task = manager.getTask(taskId); return task?.taskType ?? 'general'; }, setTokenUsage(usage) { manager.updateTask(taskId, { tokenUsage: usage }); }, }; return handle; }