import { apiRequest } from './common.ts' // ============================================================================ // Types // ============================================================================ export type TaskStatus = 'created' | 'running' | 'validating' | 'completed' | 'failed' | 'cancelled' export interface TaskInputContent { files?: Array<{ id: string name: string mimeType: string url: string }> variables?: Record variablesRichContent?: Record messages?: Array<{ role: string, content: string }> } export interface TaskOutputContent { content?: string [key: string]: any } export interface TaskMetadata { editedFields?: Array<{ path: string author: string editedAt: string previousValue?: any }> approvalRevokedBy?: string approvalRevokedAt?: string sourceFile?: { filename: string rowIndex: number totalRows?: number uploadedAt?: string uploadedBy?: string } } export interface Task { id: string reference: number name: string status: TaskStatus rawInput?: Record inputContent?: TaskInputContent outputContent?: TaskOutputContent originalOutputContent?: TaskOutputContent createdBy: string approvedBy?: string approvedAt?: string completionRunId?: string workflowRunId?: string promptVersionId: string promptApplicationId: string workspaceId: string metadata?: TaskMetadata tags?: string[] createdAt: string updatedAt: string deletedAt?: string } export interface TaskAnalytics { taskId: string firstOpenedAt?: string firstOpenedByEmail?: string approvedAt?: string approvedByEmail?: string reviewDurationSeconds?: number totalFields?: number fieldsEdited?: number accuracyPercentage?: number } export interface TaskWithAnalytics extends Task { analytics?: TaskAnalytics } export interface TaskVersion { id: string outputContent: TaskOutputContent metadata?: TaskMetadata createdBy: string taskId: string promptApplicationId: string workspaceId: string createdAt: string deletedAt?: string } // ============================================================================ // List Tasks Options // ============================================================================ export type TaskOrderBy = 'name' | 'reference' | 'approvedAt' | 'createdAt' | 'updatedAt' | 'id' | 'status' | 'approvedBy' | 'createdBy' export interface ListTasksOptions { promptApplicationId?: string promptVersionId?: string completionRunId?: string limit?: number offset?: number orderBy?: TaskOrderBy | TaskOrderBy[] order?: 'asc' | 'desc' status?: TaskStatus[] approvedAtSince?: string approvedAtUntil?: string createdAtSince?: string createdAtUntil?: string updatedAtSince?: string updatedAtUntil?: string approvedBy?: string[] | null createdBy?: string[] | null taskName?: string tags?: string[] ids?: string[] excludeInputOutputColumns?: boolean } export interface PaginationMeta { totalCount: number limit: number offset: number currentPage: number totalPages: number lastReference?: number links: { first?: string last?: string next?: string previous?: string } } export interface PaginatedTasks { data: Task[] meta: PaginationMeta } // ============================================================================ // Update Task Payload // ============================================================================ export interface UpdateTaskPayload { status?: 'completed' name?: string tags?: string[] outputContent?: TaskOutputContent } // ============================================================================ // API Functions // ============================================================================ /** * List tasks with pagination and filtering */ export async function listTasks(options?: ListTasksOptions): Promise { const params = new URLSearchParams() if (options?.promptApplicationId) params.set('promptApplicationId', options.promptApplicationId) if (options?.promptVersionId) params.set('promptVersionId', options.promptVersionId) if (options?.completionRunId) params.set('completionRunId', options.completionRunId) if (options?.limit) params.set('limit', String(options.limit)) if (options?.offset) params.set('offset', String(options.offset)) if (options?.order) params.set('order', options.order) if (options?.taskName) params.set('taskName', options.taskName) if (options?.excludeInputOutputColumns) params.set('excludeInputOutputColumns', 'true') if (options?.orderBy) { const orderByArr = Array.isArray(options.orderBy) ? options.orderBy : [options.orderBy] orderByArr.forEach(ob => params.append('orderBy', ob)) } if (options?.status?.length) { options.status.forEach(s => params.append('status', s)) } if (options?.approvedAtSince) params.set('approvedAtSince', options.approvedAtSince) if (options?.approvedAtUntil) params.set('approvedAtUntil', options.approvedAtUntil) if (options?.createdAtSince) params.set('createdAtSince', options.createdAtSince) if (options?.createdAtUntil) params.set('createdAtUntil', options.createdAtUntil) if (options?.updatedAtSince) params.set('updatedAtSince', options.updatedAtSince) if (options?.updatedAtUntil) params.set('updatedAtUntil', options.updatedAtUntil) if (options?.approvedBy) { options.approvedBy.forEach(ab => params.append('approvedBy', ab)) } if (options?.createdBy) { options.createdBy.forEach(cb => params.append('createdBy', cb)) } if (options?.tags?.length) { params.set('tags', JSON.stringify(options.tags)) } if (options?.ids?.length) { options.ids.forEach(id => params.append('ids', id)) } const queryString = params.toString() const url = queryString ? `/task?${queryString}` : '/task' return apiRequest(url) } /** * Get a single task by ID */ export async function getTask( taskId: string, options?: { includeAnalytics?: boolean }, ): Promise { let url = `/task/${taskId}` if (options?.includeAnalytics) { url += '?includeAnalytics=true' } return apiRequest(url) } /** * Update a task (status, name, tags, or outputContent) * Note: Setting status to 'completed' marks the task as approved */ export async function updateTask( taskId: string, payload: UpdateTaskPayload, ): Promise { return apiRequest(`/task/${taskId}`, { method: 'PATCH', body: JSON.stringify(payload), }) } /** * Approve a task (shorthand for updating status to 'completed') */ export async function approveTask(taskId: string): Promise { return updateTask(taskId, { status: 'completed' }) } /** * Delete a task (soft delete) */ export async function deleteTask(taskId: string): Promise<{ success: true }> { return apiRequest<{ success: true }>(`/task/${taskId}`, { method: 'DELETE', }) } /** * Delete multiple tasks (soft delete) */ export async function deleteTasks(taskIds: string[]): Promise<{ deleted: number, deletedIds: string[] }> { return apiRequest<{ deleted: number, deletedIds: string[] }>('/task/bulk', { method: 'DELETE', body: JSON.stringify(taskIds), }) } /** * Undo approval for tasks (reverts to 'validating' status) */ export async function undoApprovalTasks(taskIds: string[]): Promise<{ undoApproval: number, undoApprovalIds: string[] }> { return apiRequest<{ undoApproval: number, undoApprovalIds: string[] }>('/task/bulk/undo-approval', { method: 'POST', body: JSON.stringify(taskIds), }) } /** * Get task version history */ export async function getTaskVersions(taskId: string): Promise { return apiRequest(`/task/${taskId}/versions`) } /** * Get task completion insights by prompt version */ export async function getTaskInsightsByVersion(promptVersionId: string): Promise<{ approved: number corrected: number pending: number }> { return apiRequest(`/task/insights-by-version?promptVersionId=${promptVersionId}`) } /** * Get overall task metrics for the workspace */ export async function getTaskMetrics(): Promise<{ total: number validating: number reviewedThisWeek: number }> { return apiRequest('/task/overall-metrics') } // ============================================================================ // Task Creation // ============================================================================ /** * A file as canvas and workflow tasks represent it. Exactly these keys. */ export interface TaskFileRef { file_url: string file_name?: string mime_type?: string } /** * A value for one variable of a canvas or workflow task. * * A plain string is text. A `vault://` string, or an array of them, becomes a file value. Pass * `{ text, files }` for a variable that carries both. */ export type TaskVariableValue = | string | string[] | TaskFileRef | { files: TaskFileRef[] } | { text: string | Record, files: TaskFileRef[] } /** * A value for one variable of an **agent** task. Agents take one file per variable and use a * different key set — `{ vaultRef, filename }`, never `file_url`. */ export type AgentTaskVariableValue = string | number | boolean | Record | { vaultRef: string filename: string metadata?: string } export interface CreateTaskPayload { name: string promptApplicationId: string /** Defaults to the application's `usedVersion` when omitted. */ promptVersionId?: string rawInput?: { variables: Record } tags?: string[] } export interface CreateAgentTaskPayload { name: string rawInput?: { variables: Record } tags?: string[] } async function describeVaultFile(vaultRef: string): Promise { try { const { getFileMetadata } = await import('./vault.ts') const metadata = await getFileMetadata(vaultRef) return { file_url: vaultRef, ...(metadata?.originalFileName && { file_name: metadata.originalFileName }), ...(metadata?.mimeType && { mime_type: metadata.mimeType }), } } catch { return { file_url: vaultRef } } } function isVaultRef(value: unknown): value is string { return typeof value === 'string' && value.startsWith('vault://') } /** * Normalize a variable map for a canvas or workflow task. * * A bare `vault://` string is accepted by the API, stored verbatim, and then the run fails with no * error recorded on the task — so file values are expanded here into `{ files: [...] }` with the * name and mime type read from the Vault. Without a name the server synthesizes an extensionless * `-0`, which breaks mime detection downstream. */ export async function buildTaskVariables( variables: Record, ): Promise> { const entries = await Promise.all( Object.entries(variables).map(async ([name, value]): Promise<[string, unknown]> => { if (isVaultRef(value)) return [name, { files: [await describeVaultFile(value)] }] if (Array.isArray(value)) return [name, { files: await Promise.all(value.map(describeVaultFile)) }] if (typeof value === 'object' && value !== null && 'file_url' in value) return [name, { files: [value] }] return [name, value] }), ) return Object.fromEntries(entries) } /** * Normalize a variable map for an **agent** task. * * Agent file inputs are `{ vaultRef, filename }` and accept exactly one file — an array is * rejected. Text inputs take any JSON value; objects are stringified server-side. */ export async function buildAgentTaskVariables( variables: Record, ): Promise> { const entries = await Promise.all( Object.entries(variables).map(async ([name, value]): Promise<[string, AgentTaskVariableValue]> => { if (!isVaultRef(value)) return [name, value as AgentTaskVariableValue] const file = await describeVaultFile(value) return [name, { vaultRef: value, filename: file.file_name ?? name }] }), ) return Object.fromEntries(entries) } /** * Create tasks in a canvas or workflow Workstation. * * Every task in a batch must share one `promptApplicationId`, and a batch is capped at 100. * Requires the `reviewer` role. Always resolves to an array, even for a single task. * * Pass `rawInput` even when the prompt takes no variables: a task created without it skips the * required-variable check and runs with nothing bound, producing a meaningless result. */ export async function createTasks(payloads: CreateTaskPayload[]): Promise { const created = await apiRequest('/task', { method: 'POST', body: JSON.stringify(payloads), }) return Array.isArray(created) ? created : [created] } /** * Create one task from a plain variable map, expanding `vault://` values into file references. * * Variable names must match the application's declared variables — see `getApplicationVariables`. * A missing required variable is rejected at create time. */ export async function createTask( applicationId: string, name: string, variables: Record = {}, options?: { tags?: string[], promptVersionId?: string }, ): Promise { const [task] = await createTasks([{ name, promptApplicationId: applicationId, rawInput: { variables: await buildTaskVariables(variables) }, ...(options?.tags && { tags: options.tags }), ...(options?.promptVersionId && { promptVersionId: options.promptVersionId }), }]) return task! } /** * Create tasks for an agent. * * Agents have no prompt application — an agent task is identified by `agentId` alone, and the * database refuses one that carries a `promptApplicationId`. Batch capped at 100. * * Agent tasks also reject several prompt-task operations: versions, undo-approval, test case * creation, analytics, progress, and export. */ export async function createAgentTasks( agentId: string, tasks: CreateAgentTaskPayload[], ): Promise { const created = await apiRequest('/task', { method: 'POST', body: JSON.stringify({ sourceType: 'agent', agentId, tasks }), }) return Array.isArray(created) ? created : [created] } /** * Create one agent task from a plain variable map. */ export async function createAgentTask( agentId: string, name: string, variables: Record = {}, options?: { tags?: string[] }, ): Promise { const [task] = await createAgentTasks(agentId, [{ name, rawInput: { variables: await buildAgentTaskVariables(variables) }, ...(options?.tags && { tags: options.tags }), }]) return task! } /** * List an agent's tasks. `agentId` and `promptApplicationId` cannot be combined. */ export async function listAgentTasks( agentId: string, options?: Omit, ): Promise { const params = new URLSearchParams({ agentId }) if (options?.limit) params.set('limit', String(options.limit)) if (options?.offset) params.set('offset', String(options.offset)) if (options?.taskName) params.set('taskName', options.taskName) return apiRequest(`/task?${params.toString()}`) } /** * Re-execute a task, keeping its inputs. * * Only a `failed` task can be rerun — anything else returns * `400 Only failed tasks can be rerun`. Not available for agent tasks. */ export async function rerunTask(taskId: string): Promise<{ taskId: string executionRunId: string attemptNumber: number status: 'running' }> { return apiRequest(`/task/${taskId}/rerun`, { method: 'POST' }) } /** * Promote a real task into a regression test case on the underlying canvas, carrying its inputs. * Not available for agent tasks. */ export async function createTestCaseFromTask( taskId: string, payload?: { title?: string }, ): Promise<{ id: string }> { return apiRequest(`/task/${taskId}/create-test-case`, { method: 'POST', body: JSON.stringify(payload ?? {}), }) } /** * Execution attempts for a task, most recent first. */ export async function getTaskExecutionHistory(taskId: string): Promise { return apiRequest(`/task/${taskId}/execution-history`) } /** * Wait until a task leaves `running`. * * A settled task is `validating` (executed, awaiting human review), `completed` (approved), or * `failed`. `validating` is the normal resting state — it does not mean anything went wrong. */ export async function waitForTask( taskId: string, options?: { intervalMs?: number, maxAttempts?: number }, ): Promise { const interval = options?.intervalMs ?? 5000 const maxAttempts = options?.maxAttempts ?? 120 for (let attempt = 0; attempt < maxAttempts; attempt++) { const task = await getTask(taskId) if (task.status !== 'running' && task.status !== 'created') return task await new Promise(resolve => setTimeout(resolve, interval)) } throw new Error(`Task ${taskId} did not settle within ${(interval * maxAttempts) / 1000}s`) }